Slow Bud building due to SCSS optimizations in Sage 10 during development

Hi,

I am using Sage 10 with bud (6.6.5), bud-sass (6.6.5) and bootstrap (5.2) and I am having very long compile times that are getting on my nerves and that are wasting my time during development…when running bud dev changes to my scss code usually take 8-10 seconds to be compiled for every new build using “bud dev” or “bud build”…javascript changes on the other hand are almost applied immediately…

I have observed this with several projects that are similar in that they use bootstrap instead of tailwind but otherwise are completely different and have different amounts of assets or third party dependencies…therefore i am seeing the problem in the general bud build process…

Thanks to the post by @strarsis at Bud compiling really slow - #4 by strarsis my impression was confirmed and I was able to determine why my builds are so slow:

According to flamebearer most of the time is wasted with postcss processes induces by bud-sass like postcss-svgo, postcss-minify-selectors, postcss-merge-longhand, postcss-merge-rules, postcss-unique-selectors (combined ~60% of the build time) and cssnanominify (~40%)…

For my development process I don’t need any optimizations of my css and I would rather only have them applied only when I build for production…

I do not need anything to be minified or optimized during development using “bud dev”…changing any JS is applied immediately which is what I would like to see with my scss, too, and what I am used to from other tools like laravel mix and so on…

However for my production output of course I would like to optimize as much as possible…

So my question is: How can I disable any optimizations by postcss or any other module like “cssnanominify” when using “bud dev” while still being able to apply them when building regularly with “bud build”…?

Thanks!

Edit: this is a screenshot of my flamebearer output

What does your config look like?

By default neither roots/bud nor the roots/sage extension apply any optimization to production builds.

If you run the the command with the --debug flag and check .budfiles/sage/webpack.config.dump you can see the config as it was passed to webpack.

It should look something like this on a stock install:

yarn bud dev --debug:

  ...
  "optimization": Object {
    "emitOnErrors": true,
    "minimize": false,
    "minimizer": Array [
      "...",
    ],
    "moduleIds": "named",
    "removeEmptyChunks": true,
    "runtimeChunk": "single",
    "splitChunks": false,
  }
  ...

Are you are calling bud.minimize in your config? You will want to make sure to only do so for production builds.

Any of these will limit it (these are the default for roots/sage, by the way):

export default async bud => {
  bud.minimize(bud.isProduction)
}
export default async bud => {
  bud.when(bud.isProduction, () => bud.minimize())
}
export default async bud => {
  if (bud.isProduction) bud.minimize()
}

Anyway, sorry if that’s off base; just a hunch . A config would be helpful if you need additional help.

Thanks for your reply this is my config output when running yarn dev --debug:

Object {
  "bail": false,
  "cache": Object {
    "allowCollectingMemory": true,
    "buildDependencies": Object {
      "config": Array [
        "/site/theme/bud.config.mjs",
        "/site/theme/composer.json",
        "/site/theme/jsconfig.json",
        "/site/theme/package.json",
        "/site/theme/theme.json",
      ],
    },
    "cacheDirectory": "/site/theme/.budfiles/sage/cache/development",
    "idleTimeout": 10000,
    "idleTimeoutForInitialStore": 0,
    "name": "webpack",
    "profile": true,
    "store": "pack",
    "type": "filesystem",
    "version": "0qhudllc89fefbdeou3v7k5okwe_",
  },
  "context": "/site/theme",
  "dependencies": Array [],
  "devtool": "cheap-module-source-map",
  "entry": Object {
    "app": Object {
      "import": Array [
        "@scripts/app",
        "@styles/app",
        "@roots/bud-client/lib/hot/index.mjs?name=sage&indicator=true&overlay=true&reload=true",
      ],
    },
    "dev_helpers": Object {
      "import": Array [
        "@styles/dev_helpers",
        "@roots/bud-client/lib/hot/index.mjs?name=sage&indicator=true&overlay=true&reload=true",
      ],
    },
    "editor": Object {
      "import": Array [
        "@scripts/editor",
        "@styles/editor",
        "@roots/bud-client/lib/hot/index.mjs?name=sage&indicator=true&overlay=true&reload=true",
      ],
    },
    "klaro": Object {
      "import": Array [
        "@scripts/klaro",
        "@roots/bud-client/lib/hot/index.mjs?name=sage&indicator=true&overlay=true&reload=true",
      ],
    },
  },
  "externalsType": "var",
  "infrastructureLogging": Object {
    "console": undefined,
    "level": "none",
  },
  "mode": "development",
  "module": Object {
    "noParse": undefined,
    "rules": Array [
      Object {
        "include": Array [
          "/site/theme",
        ],
        "parser": Object {
          "requireEnsure": false,
        },
        "test": /\.(mjs|jsx?)$/,
      },
      Object {
        "oneOf": Array [
          Object {
            "include": Array [
              "/site/theme",
            ],
            "test": /\.ya?ml$/,
            "use": Array [
              Object {
                "ident": "yml",
                "loader": "yml-loader",
              },
            ],
          },
          Object {
            "generator": Object {
              "filename": "images/[name][ext]",
            },
            "include": Array [
              "/site/theme/resources",
            ],
            "test": /\.webp$/,
            "type": "asset/resource",
          },
          Object {
            "generator": Object {
              "filename": "images/[name][ext]",
            },
            "include": Array [
              "/site/theme/resources",
            ],
            "test": /\.svg$/,
            "type": "asset/resource",
          },
          Object {
            "include": Array [
              "/site/theme",
            ],
            "parser": Object {
              "parse": undefined,
            },
            "test": /\.json$/,
            "type": "json",
          },
          Object {
            "include": Array [
              "/site/theme/resources",
            ],
            "test": /\.(mjs|jsx?)$/,
            "use": Array [
              Object {
                "ident": "babel",
                "loader": "/site/theme/node_modules/babel-loader/lib/index.js",
                "options": Object {
                  "cacheDirectory": "/site/theme/.budfiles/sage/cache/development/babel",
                  "env": Object {
                    "development": Object {
                      "compact": false,
                    },
                  },
                  "plugins": Array [
                    Array [
                      "/site/theme/node_modules/@babel/plugin-transform-runtime/lib/index.js",
                      Object {
                        "helpers": false,
                      },
                    ],
                    Array [
                      "/site/theme/node_modules/@babel/plugin-proposal-object-rest-spread/lib/index.js",
                    ],
                    Array [
                      "/site/theme/node_modules/@babel/plugin-proposal-class-properties/lib/index.js",
                    ],
                    Array [
                      "/site/theme/node_modules/@babel/plugin-syntax-dynamic-import/lib/index.js",
                    ],
                  ],
                  "presets": Array [
                    Array [
                      "/site/theme/node_modules/@babel/preset-env/lib/index.js",
                    ],
                    Array [
                      "/site/theme/node_modules/@babel/preset-react/lib/index.js",
                    ],
                  ],
                  "root": "/site/theme",
                },
              },
            ],
          },
          Object {
            "generator": Object {
              "filename": "images/[name][ext]",
            },
            "include": Array [
              "/site/theme/resources",
            ],
            "test": /\.(png|jpe?g|gif|webp)$/,
            "type": "asset/resource",
          },
          Object {
            "include": Array [
              "/site/theme",
            ],
            "test": /\.(html?)$/,
            "use": Array [
              Object {
                "ident": "html",
                "loader": "html-loader",
              },
            ],
          },
          Object {
            "generator": Object {
              "filename": "fonts/[name][ext]",
            },
            "include": Array [
              "/site/theme/resources",
            ],
            "test": /\.(ttf|otf|eot|woff2?|ico)$/,
            "type": "asset",
          },
          Object {
            "include": Array [
              "/site/theme/resources",
            ],
            "test": /\.module\.css$/,
            "use": Array [
              Object {
                "ident": "style",
                "loader": "style-loader",
              },
              Object {
                "ident": "cssModule",
                "loader": "css-loader",
                "options": Object {
                  "esModule": true,
                  "importLoaders": 1,
                  "localIdentName": "[name]__[local]___[hash:base64:5]",
                  "modules": true,
                  "sourceMap": true,
                },
              },
              Object {
                "ident": "postcss",
                "loader": "/site/theme/node_modules/postcss-loader/dist/cjs.js",
                "options": Object {
                  "postcssOptions": Object {
                    "plugins": Array [
                      "/site/theme/node_modules/postcss-import/index.js",
                      "/site/theme/node_modules/postcss-nested/index.js",
                      Array [
                        "/site/theme/node_modules/postcss-preset-env/dist/index.cjs",
                        Object {
                          "features": Object {
                            "focus-within-pseudo-class": false,
                          },
                          "stage": 1,
                        },
                      ],
                    ],
                    "syntax": "postcss-scss",
                  },
                  "sourceMap": true,
                },
              },
            ],
          },
          Object {
            "include": Array [
              "/site/theme/resources",
            ],
            "test": /\.css$/,
            "use": Array [
              Object {
                "ident": "style",
                "loader": "style-loader",
              },
              Object {
                "ident": "css",
                "loader": "css-loader",
                "options": Object {
                  "importLoaders": 1,
                  "modules": false,
                  "sourceMap": true,
                },
              },
              Object {
                "ident": "postcss",
                "loader": "/site/theme/node_modules/postcss-loader/dist/cjs.js",
                "options": Object {
                  "postcssOptions": Object {
                    "plugins": Array [
                      "/site/theme/node_modules/postcss-import/index.js",
                      "/site/theme/node_modules/postcss-nested/index.js",
                      Array [
                        "/site/theme/node_modules/postcss-preset-env/dist/index.cjs",
                        Object {
                          "features": Object {
                            "focus-within-pseudo-class": false,
                          },
                          "stage": 1,
                        },
                      ],
                    ],
                    "syntax": "postcss-scss",
                  },
                  "sourceMap": true,
                },
              },
            ],
          },
          Object {
            "include": Array [
              "/site/theme/resources",
            ],
            "test": /\.(scss|sass)$/,
            "use": Array [
              Object {
                "ident": "style",
                "loader": "style-loader",
              },
              Object {
                "ident": "css",
                "loader": "css-loader",
                "options": Object {
                  "importLoaders": 1,
                  "modules": false,
                  "sourceMap": true,
                },
              },
              Object {
                "ident": "postcss",
                "loader": "/site/theme/node_modules/postcss-loader/dist/cjs.js",
                "options": Object {
                  "postcssOptions": Object {
                    "plugins": Array [
                      "/site/theme/node_modules/postcss-import/index.js",
                      "/site/theme/node_modules/postcss-nested/index.js",
                      Array [
                        "/site/theme/node_modules/postcss-preset-env/dist/index.cjs",
                        Object {
                          "features": Object {
                            "focus-within-pseudo-class": false,
                          },
                          "stage": 1,
                        },
                      ],
                    ],
                    "syntax": "postcss-scss",
                  },
                  "sourceMap": true,
                },
              },
              Object {
                "ident": "resolveUrl",
                "loader": "/site/theme/node_modules/resolve-url-loader/index.js",
                "options": Object {
                  "root": "/site/theme/resources",
                  "sourceMap": true,
                },
              },
              Object {
                "ident": "sass-loader",
                "loader": "sass-loader",
                "options": Object {
                  "implementation": Object {
                    "Exception": [Function sass.Exception],
                    "FALSE": SassBoolean0 {
                      "value": false,
                    },
                    "Logger": Object {
                      "silent": Object {
                        "debug": [Function sass.Logger.silent.debug],
                        "warn": [Function sass.Logger.silent.warn],
                      },
                    },
                    "NULL": _SassNull0 {},
                    "SassArgumentList": [Function sass.SassArgumentList],
                    "SassBoolean": [Function sass.SassBoolean],
                    "SassColor": [Function sass.SassColor],
                    "SassFunction": [Function sass.SassFunction],
                    "SassList": [Function sass.SassList],
                    "SassMap": [Function sass.SassMap],
                    "SassNumber": [Function sass.SassNumber],
                    "SassString": [Function sass.SassString],
                    "TRUE": SassBoolean0 {
                      "value": true,
                    },
                    "Value": [Function Value0],
                    "cli_pkg_main_0_": [Function anonymous],
                    "compile": [Function sass.compile],
                    "compileAsync": [Function sass.compileAsync],
                    "compileString": [Function sass.compileString],
                    "compileStringAsync": [Function sass.compileStringAsync],
                    "info": "dart-sass	1.56.2	(Sass Compiler)	[Dart]
dart2js	2.18.5	(Dart Compiler)	[Dart]",
                    "load": [Function anonymous],
                    "render": [Function sass.render],
                    "renderSync": [Function sass.renderSync],
                    "sassFalse": SassBoolean0 {
                      "value": false,
                    },
                    "sassNull": _SassNull0 {},
                    "sassTrue": SassBoolean0 {
                      "value": true,
                    },
                    "types": Object {
                      "Boolean": [Function sass.types.Boolean],
                      "Color": [Function sass.types.Color],
                      "Error": [Function Error],
                      "List": [Function sass.types.List],
                      "Map": [Function sass.types.Map],
                      "Null": [Function sass.types.Null],
                      "Number": [Function sass.types.Number],
                      "String": [Function sass.types.String],
                    },
                  },
                  "sourceMap": true,
                },
              },
            ],
          },
        ],
      },
    ],
    "unsafeCache": false,
  },
  "name": "sage",
  "node": false,
  "optimization": Object {
    "emitOnErrors": true,
    "minimize": false,
    "minimizer": Array [
      "...",
    ],
    "moduleIds": undefined,
    "runtimeChunk": undefined,
    "splitChunks": Object {
      "automaticNameDelimiter": "/",
      "cacheGroups": Object {
        "vendor": Object {
          "filename": "js/bundle/vendor/[name].js",
          "idHint": "vendor",
          "priority": -20,
          "test": /[\\/]node_modules[\\/]/,
        },
      },
      "chunks": "all",
      "minSize": 0,
    },
  },
  "output": Object {
    "assetModuleFilename": "[name][ext]",
    "chunkFilename": "js/dynamic/[id].js",
    "clean": false,
    "environment": undefined,
    "filename": "js/[name].js",
    "module": undefined,
    "path": "/site/theme/public",
    "pathinfo": undefined,
    "publicPath": "/",
    "uniqueName": "sage",
  },
  "parallelism": 110,
  "performance": Object {
    "hints": false,
  },
  "plugins": Array [
    CopyPlugin {
      "options": Object {},
      "patterns": Array [
        Object {
          "context": "/site/theme/resources",
          "from": "/site/theme/resources/images",
          "noErrorOnMissing": true,
          "to": "/site/theme/public/images/[path][name][ext]",
          "toType": "template",
        },
      ],
    },
    HotModuleReplacementPlugin {
      "options": Object {},
    },
    WebpackManifestPlugin {
      "options": Object {
        "assetHookStage": Infinity,
        "basePath": "",
        "fileName": "manifest.json",
        "filter": null,
        "generate": undefined,
        "map": null,
        "publicPath": "",
        "removeKeyHash": /([a-f0-9]{16,32}\.?)/gi,
        "seed": undefined,
        "serialize": [Function serialize],
        "sort": null,
        "transformExtensions": /^(gz|map)$/i,
        "useEntryKeys": false,
        "useLegacyEmit": false,
        "writeToFileEmit": true,
      },
    },
    MergedManifestWebpackPlugin {
      "entrypointsName": "entrypoints.json",
      "file": "entrypoints.json",
      "plugin": Object {
        "name": "MergedManifestPlugin",
      },
      "wordpressName": "wordpress.json",
    },
    WordPressDependenciesWebpackPlugin {
      "fileName": "wordpress.json",
      "manifest": Object {},
      "plugin": Object {
        "name": "WordPressDependenciesWebpackPlugin",
        "stage": Infinity,
      },
      "usedDependencies": Object {},
    },
    WordPressExternals {
      "externals": ExternalsPlugin {
        "externals": [Function externals],
        "type": "window",
      },
      "name": "WordPressExternalsWebpackPlugin",
      "stage": Infinity,
    },
    EntrypointsWebpackPlugin {
      "name": "entrypoints.json",
      "options": Object {
        "emitHtml": false,
        "publicPath": "",
      },
      "plugin": Object {
        "name": "EntrypointsManifestPlugin",
        "stage": Infinity,
      },
    },
  ],
  "recordsPath": "/site/theme/.budfiles/sage/modules.json",
  "resolve": Object {
    "alias": Object {
      "@dist": "/site/theme/public",
      "@fonts": "/site/theme/resources/fonts",
      "@images": "/site/theme/resources/images",
      "@scripts": "/site/theme/resources/scripts",
      "@src": "/site/theme/resources",
      "@styles": "/site/theme/resources/styles",
    },
    "extensions": Array [
      ".mjs",
      ".js",
      ".jsx",
      ".css",
      ".json",
      ".wasm",
      ".yml",
      ".scss",
      ".sass",
    ],
    "modules": Array [
      "/site/theme/resources",
      "node_modules",
    ],
  },
  "stats": Object {
    "preset": "none",
  },
  "target": "browserslist:/site/theme/package.json",
}

This is my bud.config.mjs:

/**
 * Build configuration
 *
 * @see {@link https://bud.js.org/guides/getting-started/configure}
 * @param {import('@roots/bud').Bud} app
 */
export default async (app) => {

  app
    /**
     * Application entrypoints
     */
    .entry({
      app: ["@scripts/app", "@styles/app"],
      dev_helpers: ["@styles/dev_helpers"],
      editor: ["@scripts/editor", "@styles/editor"],
      klaro: ["@scripts/klaro"],
    })




    /**
     * Directory contents to be included in the compilation
     */
    .assets(["images"])

    /** for development only: disable chunking and hashing */
    .splitChunks(true)

    /**
     * Matched files trigger a page reload when modified
     */
    .watch(["resources/views/**/*", "resources/html/**/*", "app/**/*"])

    /**
     * Proxy origin (`WP_HOME`)
     */
    .proxy(app.env.get('WP_HOME') ? app.env.get('WP_HOME') : "http://localhost:8090")

    /**
     * Development origin
     */
    .serve("http://127.0.0.1:3000")

    /**
     * URI of the `public` directory
     */
    .setPublicPath("/app/themes/my-theme/public/");
};

I would remove the call to bud.splitChunks entirely. You don’t need to create a vendor bundle for development – this has got to be slowing the build down.

Here is the relevant bit from the roots/sage base config. It should be pretty well tuned for most projects without you needing to do anything:

Thanks, I had been fiddeling with this before…I just removed it from my config but it does not seem to make any difference…without it build time using “yarn build” is 14.5s … “bud dev” not responding faster than before, neither (9.7s according to console output)…

It seems that bud-sass is the module that introduces postcss in the build process…how can I tell it to not optimize anything when building for development? or how can I just disable postcss in general for all extensions?

I don’t think this is going to do what you want it to do and I don’t think you want to disable postcss-preset-env.

The minify stuff you have a problem with is hoisted from css-minimizer-webpack-plugin, not anything related to postcss or its presets.

❯ yarn why postcss-minify-selectors
=> Found "postcss-minify-selectors@5.2.1"
info Reasons this module exists
   - "@roots#bud#@roots#bud-support#css-minimizer-webpack-plugin#cssnano#cssnano-preset-default" depends on it
❯ yarn why postcss-svgo
=> Found "postcss-svgo@5.1.0"
info Reasons this module exists
   - "@roots#bud#@roots#bud-support#css-minimizer-webpack-plugin#cssnano#cssnano-preset-default" depends on it
   - Hoisted from "@roots#bud#@roots#bud-support#css-minimizer-webpack-plugin#cssnano#cssnano-preset-default#postcss-svgo"
❯ yarn why cssnano   
=> Found "cssnano@5.1.14"
info Reasons this module exists
   - "@roots#bud#@roots#bud-support#css-minimizer-webpack-plugin" depends on it
   - Hoisted from "@roots#bud#@roots#bud-support#css-minimizer-webpack-plugin#cssnano"

flamegraph highlighting processes with css in the name:

Taken from sage with these deps:

  "devDependencies": {
    "@roots/bud": "^2023.2.8",
    "@roots/bud-sass": "^2023.2.8",
    "@roots/bud-swc": "^2023.2.8",
    "@roots/bud-tailwindcss": "^2023.2.8",
    "@roots/sage": "^2023.2.8"
  },

Basically all postcss is going to do by default is run autoprefixer based on what you have in browserslist.

I don’t know why your minifier is running but configuring postcss is the wrong tree. That said, there is detailed documentation for managing postcss here: @roots/bud-postcss | bud.js. If you want to manipulate the webpack config directly you can use bud.webpackConfig:

bud.webpackConfig(config => {
  config.optimization = undefined

  return config
})
1 Like

Thanks for your explanations…I am not sure how to optimize it yet (configuring webpackConfig with config.optimization = undefined seems to make the whole process even slower after my first try) but I will try to find a solution and will go through the bud-postcss config in more depth…

I don’t know why your minifier is running but configuring postcss is the wrong tree.

As I said I did not really change much about the Sage vanilla config, except for adding bud-scss and bootstrap…I am using this configuration in most of my projects and slow build processes as described above consitently is my experience with this setup…maybe at some point you can look into why this config is so slow as I am sure it could help other people using a similar setup…thank you!

If you are adding optimization: undefined to your webpack config then the defaults from webpack are going to be used. Webpack applies optimizations automatically if certain conditions are met.

Better would be to explicitly disable the minimizer: {optimization: {minimize: false}}. This is what bud does by default.

I just tested main of roots/sage and performance seems fine.

app.scss:

@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";

body {
  background: $body-bg;
  font-family: $font-family-monospace;
  font-size: $font-size-base;
  color: $body-color;
}

timings for several edits:

└─ compiled 44 modules in 139ms
└─ compiled 44 modules in 100ms
└─ compiled 44 modules in 122ms
└─ compiled 44 modules in 92ms

I don’t know anything about your application, but I’m guessing the following info might be helpful:

If I import all of bootstrap in app.scss things get a lot slower, because I am recompiling hundreds of modules (~1.5 seconds).

It is unnecessary to recompile them on every edit, though, since bootstrap isn’t changing. Instead, I would prefer to include my library build in a separate module:

styles/bootstrap.scss:

@import 'bootstrap/scss/bootstrap';
// or import partials and do overrides, whatever

And keep app.scss specific to my application:

styles/app.scss:

// it's fine to import variables and functions I'm going to use
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";

body {
  background: $body-bg;
  font-family: $font-family-sans-serif;
  font-size: $font-size-base;
  color: $body-color;
}

Finally, instead of importing bootstrap.scss into app.scss I list it as a separate module in the entrypoint:

bud.config.js:

  app
    /**
     * Treat bootstrap as source modules
     */
    .compilePaths([
      app.path(`resources`),
      app.path(`node_modules/bootstrap`),
    ])
    
    /**
     * Application entrypoints
     * @see {@link https://bud.js.org/docs/bud.entry/}
     */
    .entry({
      app: ['@styles/bootstrap', '@scripts/app', '@styles/app'],
      editor: ['@scripts/editor', '@styles/editor'],
    })

Which brings the hmr roundtrip back down to ~100ms since edits to app.scss don’t invalidate bootstrap.scss.

2 Likes