Summary
When we switched to using Webpack for handling our SASS files, we noticed that our build times in some cases became really slow. After measuring the performance of different parts of the build using the SpeedMeasurePlugin, it seems that sass-loader is the culprit…it can easily take 10s to build (it used to take 20s before we made some fixes), which is longer than we’d like.
I’m curious if people have additional strategies for optimizing building Sass assets that I didn’t cover. I’ve gone through a fair number of them at this point (multiple times for some of them), and still can’t seem to get the build times low enough. In terms of goals, currently a large rebuild (such as making a change for a component used in many files), can easily take 10-12 seconds, I’m hoping to get it down closer to 5s if possible.
Solutions Tried
We tried a number of different solutions, some worked, others did not help much.
- HardSourcePlugin - This one worked reasonably well. Depending on the caching for that build, it was able to reduce build times by several seconds.
- Removing duplicated imports (like importing the same ‘variables.sass’ file into multiple places) - This also reduced build time by a few seconds
- Changing our mix of SASS and SCSS to just SCSS - I’m not sure why this helped, but it did seem to shave a bit off our build times. Maybe because everything was the same file type, it was easier to compile? (It’s possible that something else was happening here to confound the results, but it did seem to consistently help).
- Replace sass-loader with fast-sass-loader - Many people recommended this, but when I got it to work, it didn’t seem to change the build time at all. I’m not sure why…maybe there was a configuration issue.
- Utilizing cache-loader - This also didn’t seem to have any improvement.
- Disabled source maps for Sass - This seems to have had a big effect, reducing the build times in half (from when the change was applied)
- Tried using
includePaths
for SASS loaded from node_modules - This was suggested on a git issue I found, where sass-loader had issues with something they called ’custom importers’. My understanding was that by using includePaths, SASS was able to rely on those provided absolute paths, instead of using an inefficient algorithm for resolving paths to places like node_modules
From some brief stats, we appear to have about 16k lines of SASS code spread across 150 SASS files. Some have a fair amount of code, while others have less, with a simple average of the LOC across these files of about 107 LOC / file.
Below is the configuration that is being used. The application is a Rails application, and so much of the Webpack configuration is handled through the Webpacker gem.
{
"mode": "production",
"output": {
"filename": "js/[name].js",
"chunkFilename": "js/[name].js",
"hotUpdateChunkFilename": "js/[id]-[hash].hot-update.js",
"path": "myApp/public/packs",
"publicPath": "/packs/"
},
"resolve": {
"extensions": [".mjs", ".js", ".sass", ".scss", ".css", ".module.sass", ".module.scss", ".module.css", ".png", ".svg", ".gif", ".jpeg", ".jpg"],
"plugins": [{
"topLevelLoader": {}
}],
"modules": ["myApp/app/assets/javascript", "myApp/app/assets/css", "node_modules"]
},
"resolveLoader": {
"modules": ["node_modules"],
"plugins": [{}]
},
"node": {
"dgram": "empty",
"fs": "empty",
"net": "empty",
"tls": "empty",
"child_process": "empty"
},
"devtool": "source-map",
"stats": "normal",
"bail": true,
"optimization": {
"minimizer": [{
"options": {
"test": {},
"extractComments": false,
"sourceMap": true,
"cache": true,
"parallel": true,
"terserOptions": {
"output": {
"ecma": 5,
"comments": false,
"ascii_only": true
},
"parse": {
"ecma": 8
},
"compress": {
"ecma": 5,
"warnings": false,
"comparisons": false
},
"mangle": {
"safari10": true
}
}
}
}],
"splitChunks": {
"chunks": "all",
"name": false
},
"runtimeChunk": true
},
"externals": {
"moment": "moment"
},
"entry": {
"entry1": "myApp/app/assets/javascript/packs/entry1.js",
"entry2": "myApp/app/assets/javascript/packs/entry2.js",
"entry3": "myApp/app/assets/javascript/packs/entry3.js",
"entry4": "myApp/app/assets/javascript/packs/entry4.js",
"entry5": "myApp/app/assets/javascript/packs/entry5.js",
"entry6": "myApp/app/assets/javascript/packs/entry6.js",
"entry7": "myApp/app/assets/javascript/packs/entry7.js",
"entry8": "myApp/app/assets/javascript/packs/entry8.js",
"landing": "myApp/app/assets/javascript/packs/landing.js",
"entry9": "myApp/app/assets/javascript/packs/entry9.js",
"entry10": "myApp/app/assets/javascript/packs/entry10.js",
"entry11": "myApp/app/assets/javascript/packs/entry11.js",
"entry12": "myApp/app/assets/javascript/packs/entry12.js",
"entry13": "myApp/app/assets/javascript/packs/entry13.js",
"entry14": "myApp/app/assets/javascript/packs/entry14.js",
"entry15": "myApp/app/assets/javascript/packs/entry15.js"
},
"module": {
"strictExportPresence": true,
"rules": [{
"parser": {
"requireEnsure": false
}
}, {
"test": {},
"use": [{
"loader": "file-loader",
"options": {
"context": "app/assets/javascript"
}
}]
}, {
"test": {},
"use": ["myApp/node_modules/mini-css-extract-plugin/dist/loader.js", {
"loader": "css-loader",
"options": {
"sourceMap": true,
"importLoaders": 2,
"localIdentName": "[name]__[local]___[hash:base64:5]",
"modules": false
}
}, {
"loader": "postcss-loader",
"options": {
"config": {
"path": "myApp"
},
"sourceMap": true
}
}],
"sideEffects": true,
"exclude": {}
}, {
"test": {},
"use": ["myApp/node_modules/mini-css-extract-plugin/dist/loader.js", {
"loader": "css-loader",
"options": {
"sourceMap": true,
"importLoaders": 2,
"localIdentName": "[name]__[local]___[hash:base64:5]",
"modules": false
}
}, {
"loader": "postcss-loader",
"options": {
"config": {
"path": "myApp"
},
"sourceMap": false,
"plugins": [null, null]
}
}, {
"loader": "sass-loader",
"options": {
"sourceMap": false,
"sourceComments": true
}
}],
"sideEffects": true,
"exclude": {}
}, {
"test": {},
"use": ["myApp/node_modules/mini-css-extract-plugin/dist/loader.js", {
"loader": "css-loader",
"options": {
"sourceMap": true,
"importLoaders": 2,
"localIdentName": "[name]__[local]___[hash:base64:5]",
"modules": true
}
}, {
"loader": "postcss-loader",
"options": {
"config": {
"path": "myApp"
},
"sourceMap": true
}
}],
"sideEffects": false,
"include": {}
}, {
"test": {},
"use": ["myApp/node_modules/mini-css-extract-plugin/dist/loader.js", {
"loader": "css-loader",
"options": {
"sourceMap": true,
"importLoaders": 2,
"localIdentName": "[name]__[local]___[hash:base64:5]",
"modules": true
}
}, {
"loader": "postcss-loader",
"options": {
"config": {
"path": "myApp"
},
"sourceMap": true
}
}, {
"loader": "sass-loader",
"options": {
"sourceMap": true
}
}],
"sideEffects": false,
"include": {}
}, {
"test": {},
"include": {},
"exclude": {},
"use": [{
"loader": "babel-loader",
"options": {
"babelrc": false,
"presets": [
["@babel/preset-env", {
"modules": false
}]
],
"cacheDirectory": "tmp/cache/webpacker/babel-loader-node-modules",
"cacheCompression": true,
"compact": false,
"sourceMaps": false
}
}]
}, {
"test": {},
"include": ["myApp/app/assets/javascript", "myApp/app/assets/css"],
"exclude": {},
"use": [{
"loader": "babel-loader",
"options": {
"cacheDirectory": "tmp/cache/webpacker/babel-loader-node-modules",
"cacheCompression": true,
"compact": true
}
}]
}, {
"test": "myApp/node_modules/jquery/dist/jquery.js",
"use": [{
"loader": "expose-loader",
"options": "jQuery"
}, {
"loader": "expose-loader",
"options": "$"
}]
}, {
"test": "myApp/node_modules/popper.js/dist/umd/popper.js",
"use": [{
"loader": "expose-loader",
"options": "Popper"
}]
}, {
"test": "myApp/node_modules/scroll-depth/jquery.scrolldepth.js",
"use": [{
"loader": "expose-loader",
"options": "scrollDepth"
}]
}]
},
"plugins": [{
"environment_variables_plugin": "values don't really matter in this case I think"
}, {
"options": {},
"pathCache": {},
"fsOperations": 0,
"primed": false
}, {
"options": {
"filename": "css/[name]-[contenthash:8].css",
"chunkFilename": "css/[name]-[contenthash:8].chunk.css"
}
}, {}, {
"options": {
"test": {},
"cache": true,
"compressionOptions": {
"level": 9
},
"filename": "[path].gz[query]",
"threshold": 0,
"minRatio": 0.8,
"deleteOriginalAssets": false
}
}, {
"pluginDescriptor": {
"name": "OptimizeCssAssetsWebpackPlugin"
},
"options": {
"assetProcessors": [{
"phase": "compilation.optimize-chunk-assets",
"regExp": {}
}],
"assetNameRegExp": {},
"cssProcessorOptions": {},
"cssProcessorPluginOptions": {}
},
"phaseAssetProcessors": {
"compilation.optimize-chunk-assets": [{
"phase": "compilation.optimize-chunk-assets",
"regExp": {}
}],
"compilation.optimize-assets": [],
"emit": []
},
"deleteAssetsMap": {}
}, {
"definitions": {
"$": "jquery",
"jQuery": "jquery",
"jquery": "jquery",
"window.$": "jquery",
"window.jQuery": "jquery",
"window.jquery": "jquery",
"Popper": ["popper.js", "default"]
}
}, {
"definitions": {
"process.env": {
"MY_DEFINED_ENV_VARS": "my defined env var values"
}
}
}, {
"options": {}
}]
}
"test": {},
empty – Septuplettest: {}
is due to JSON.stringify not serializing regex objects. – Marqueztest
bit. I never really solved it. Some changes were made that shaved off a few seconds here or there, but it's still slow. I think some of it was also architectural, which isn't something that sass-loader will really address much. – Supernovatest
is definitely due toJSON.stringify
- try definingRegExp.prototype.toJSON = RegExp.prototype.toString;
above where you stringify the Webpacker environment config (useful for debugging) – Fribblesass-loader
itself. For future readers, check if you're using thesass
package or thenode-sass
package. From my testing, I've foundnode-sass
to be incredibly slow for complex projects, whereassass
is significantly faster. – Passing