Performance Optimization and Compilation
1. Output Style Configuration (compressed, expanded)
| Output Style | Description | Use Case | Size Impact |
|---|---|---|---|
| compressed | Minified, single line | Production deployment | Smallest (30-40% reduction) |
| expanded | Standard CSS formatting | Development, debugging | Largest, most readable |
| compact | Each rule on one line | Legacy compatibility | Medium compression |
| nested | Nested like SCSS source | Understanding output structure | Medium, preserves hierarchy |
Example: CLI output style configuration
// Compressed (production)
sass --style=compressed input.scss output.css
// Expanded (development)
sass --style=expanded input.scss output.css
// With watch mode
sass --watch --style=compressed src/scss:dist/css
// Multiple files
sass --style=compressed src/scss:dist/css --no-source-map
Example: Package.json scripts configuration
{
"scripts": {
"sass:dev": "sass --watch --style=expanded src/scss:dist/css",
"sass:build": "sass --style=compressed --no-source-map src/scss:dist/css",
"sass:prod": "sass --style=compressed src/scss:dist/css && postcss dist/css/*.css --replace",
"sass:analyze": "sass --style=expanded src/scss:dist/css --embed-sources"
},
"devDependencies": {
"sass": "^1.69.0",
"postcss": "^8.4.31",
"autoprefixer": "^10.4.16",
"cssnano": "^6.0.1"
}
}
Example: Node.js API configuration
const sass = require('sass');
const fs = require('fs');
// Development build
const devResult = sass.compile('src/main.scss', {
style: 'expanded',
sourceMap: true,
sourceMapIncludeSources: true
});
fs.writeFileSync('dist/main.css', devResult.css);
fs.writeFileSync('dist/main.css.map', JSON.stringify(devResult.sourceMap));
// Production build
const prodResult = sass.compile('src/main.scss', {
style: 'compressed',
sourceMap: false,
quietDeps: true,
verbose: false
});
fs.writeFileSync('dist/main.min.css', prodResult.css);
// Output comparison
console.log(`Dev size: ${devResult.css.length} bytes`);
console.log(`Prod size: ${prodResult.css.length} bytes`);
console.log(`Reduction: ${((1 - prodResult.css.length / devResult.css.length) * 100).toFixed(1)}%`);
2. Source Map Generation and Debugging
| Option | Flag | Description | Impact |
|---|---|---|---|
| Source Map | --source-map |
Generate .css.map file | Enable DevTools debugging |
| Inline Map | --embed-source-map |
Embed map in CSS | Single file, larger CSS |
| Embed Sources | --embed-sources |
Include SCSS in map | Full debugging without files |
| No Source Map | --no-source-map |
Disable map generation | Production, smallest output |
| Source Map URLs | --source-map-urls |
absolute/relative paths | Control URL resolution |
Example: Source map configuration for different environments
// Development - Full debugging capability
sass --watch \
--style=expanded \
--source-map \
--embed-sources \
src/scss:dist/css
// Staging - External source maps
sass --style=compressed \
--source-map \
--source-map-urls=relative \
src/scss:dist/css
// Production - No source maps
sass --style=compressed \
--no-source-map \
src/scss:dist/css
// Debug mode - Everything embedded
sass --style=expanded \
--embed-source-map \
--embed-sources \
src/main.scss dist/main.css
Example: Webpack sass-loader configuration
// webpack.config.js
module.exports = {
mode: process.env.NODE_ENV || 'development',
devtool: 'source-map',
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 2
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: true
}
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
sassOptions: {
outputStyle: process.env.NODE_ENV === 'production'
? 'compressed'
: 'expanded',
sourceMapContents: true,
sourceMapEmbed: false
}
}
}
]
}
]
}
};
Example: Browser DevTools debugging workflow
// styles.scss (source file)
.button {
$base-color: #1976d2;
background: $base-color;
color: white;
&:hover {
background: darken($base-color, 10%);
}
}
// In browser DevTools with source maps:
// 1. Open DevTools → Sources tab
// 2. Navigate to webpack:// or scss:// folder
// 3. See original .scss files with line numbers
// 4. Set breakpoints (for CSS changes)
// 5. Inspect shows:
// styles.scss:4 → background: $base-color;
// Not: styles.css:1 → background: #1976d2;
// Source map enables:
// - See which .scss file generated each rule
// - View original variable names and mixins
// - Edit SCSS directly in DevTools (with workspaces)
// - Accurate error reporting with SCSS line numbers
3. Build Tool Integration (Webpack, Vite, Parcel)
| Tool | Loader/Plugin | Configuration | Features |
|---|---|---|---|
| Webpack | sass-loader | Complex, powerful | Full control, optimization |
| Vite | Built-in | Zero-config | Fast HMR, modern |
| Parcel | Built-in | Zero-config | Auto-install, simple |
| esbuild | esbuild-sass-plugin | Minimal config | Extremely fast |
| Rollup | rollup-plugin-scss | Plugin-based | Library bundling |
Example: Vite configuration (recommended for modern projects)
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
// Global variables/mixins available in all components
additionalData: `@use "src/styles/variables" as *;`,
// Modern Sass API
api: 'modern-compiler',
// Include paths for @use/@import resolution
includePaths: ['node_modules', 'src/styles']
}
},
// PostCSS configuration
postcss: {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true }
}]
})
]
},
devSourcemap: true
},
build: {
cssCodeSplit: true,
cssMinify: true,
sourcemap: true
}
});
Example: Webpack 5 comprehensive configuration
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const path = require('path');
module.exports = {
mode: process.env.NODE_ENV || 'development',
module: {
rules: [
{
test: /\.scss$/,
use: [
// Extract to separate file
MiniCssExtractPlugin.loader,
// CSS loader
{
loader: 'css-loader',
options: {
importLoaders: 2,
sourceMap: true,
modules: {
auto: true,
localIdentName: '[name]__[local]--[hash:base64:5]'
}
}
},
// PostCSS (autoprefixer, etc.)
{
loader: 'postcss-loader',
options: {
sourceMap: true,
postcssOptions: {
plugins: ['autoprefixer', 'cssnano']
}
}
},
// Sass compiler
{
loader: 'sass-loader',
options: {
sourceMap: true,
sassOptions: {
outputStyle: 'compressed',
includePaths: [
path.resolve(__dirname, 'src/styles'),
path.resolve(__dirname, 'node_modules')
]
},
// Global imports
additionalData: `
@use 'sass:math';
@use 'variables' as *;
@use 'mixins' as *;
`
}
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css'
})
],
optimization: {
minimizer: [
`...`, // Extend existing minimizers
new CssMinimizerPlugin({
minimizerOptions: {
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true,
colormin: true,
minifyFontValues: true
}]
}
})
]
}
};
Example: Parcel zero-config setup
// package.json
{
"name": "my-project",
"scripts": {
"dev": "parcel src/index.html",
"build": "parcel build src/index.html --no-source-maps"
},
"devDependencies": {
"parcel": "^2.10.0",
"sass": "^1.69.0"
}
}
// .sassrc.json (optional configuration)
{
"includePaths": ["src/styles", "node_modules"],
"outputStyle": "compressed",
"sourceMap": true
}
// index.html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="./styles/main.scss">
</head>
<body>
<!-- Content -->
</body>
</html>
// Parcel automatically:
// 1. Detects .scss files
// 2. Installs sass if needed
// 3. Compiles SCSS → CSS
// 4. Applies PostCSS transformations
// 5. Minifies for production
// 6. Generates source maps
Example: esbuild plugin configuration (fastest build)
// build.js
const esbuild = require('esbuild');
const sassPlugin = require('esbuild-sass-plugin').default;
esbuild.build({
entryPoints: ['src/main.scss'],
outfile: 'dist/main.css',
bundle: true,
minify: true,
sourcemap: true,
plugins: [
sassPlugin({
// Sass options
style: 'compressed',
sourceMap: true,
// PostCSS integration
async transform(source, resolveDir) {
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const result = await postcss([autoprefixer])
.process(source, { from: undefined });
return result.css;
}
})
],
loader: {
'.scss': 'css',
'.sass': 'css'
}
}).then(() => {
console.log('Build complete!');
}).catch(() => process.exit(1));
4. Compilation Speed Optimization Techniques
| Technique | Method | Impact | Trade-off |
|---|---|---|---|
| Use @use/@forward | Replace @import | 20-40% faster | Migration effort |
| Limit Nesting | Max 3-4 levels | 10-20% faster | Flatter structure |
| Avoid @extend | Use @mixin instead | 15-25% faster | Slightly larger CSS |
| Cache Dependencies | Enable build caching | 50-80% faster rebuilds | Disk space |
| Dart Sass | Use modern compiler | 2-3x faster than node-sass | None (recommended) |
| Partial Imports | Import only needed files | Proportional to reduction | Manual management |
| Parallel Processing | Multiple workers | 30-50% on multi-core | Complex setup |
Example: Migration to @use for better performance
// ❌ SLOW: Old @import (loads everything, no namespacing)
@import 'variables';
@import 'mixins';
@import 'functions';
@import 'components/button';
@import 'components/card';
// File loaded multiple times if imported elsewhere
// ✅ FAST: Modern @use (loads once, namespaced, tree-shakeable)
@use 'variables' as vars;
@use 'mixins' as mix;
@use 'functions' as fn;
@use 'components/button';
@use 'components/card';
// Each file loaded exactly once across entire project
// Compiler can optimize unused exports
// Explicit namespacing prevents conflicts
// Performance comparison:
// @import: ~800ms compile time
// @use: ~450ms compile time (44% faster)
Example: Webpack caching configuration
// webpack.config.js
module.exports = {
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
buildDependencies: {
config: [__filename]
}
},
module: {
rules: [
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
// Enable CSS modules caching
modules: {
mode: 'local',
localIdentContext: path.resolve(__dirname, 'src'),
// Consistent hashing for better caching
localIdentHashFunction: 'md4',
localIdentHashDigest: 'base64',
localIdentHashDigestLength: 8
}
}
},
{
loader: 'sass-loader',
options: {
// Enable Sass result caching
sassOptions: {
// Use modern compiler
api: 'modern',
// Silence dependency warnings for faster compilation
quietDeps: true,
verbose: false
}
}
}
]
}
]
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
type: 'css/mini-extract',
chunks: 'all',
enforce: true
}
}
}
}
};
// First build: ~2500ms
// Cached rebuild: ~450ms (82% faster)
Example: Optimized file structure for fast compilation
// ❌ SLOW: Monolithic structure
// main.scss imports everything
@use 'base'; // 50 variables
@use 'mixins'; // 30 mixins
@use 'utilities'; // 100 utility classes
// Every component recompiles when any part changes
// ✅ FAST: Modular structure with targeted imports
// abstracts/_index.scss
@forward 'variables';
@forward 'functions';
@forward 'mixins';
// components/button/_button.scss
@use '../../abstracts' as *;
// Only loads what's needed
// components/card/_card.scss
@use '../../abstracts' as *;
// Shared abstracts loaded once by compiler
// main.scss
@use 'abstracts';
@use 'components/button';
@use 'components/card';
// File organization for speed:
styles/
├── abstracts/
│ ├── _index.scss # Forward only
│ ├── _variables.scss # Pure data (fast)
│ ├── _functions.scss # Pure functions (fast)
│ └── _mixins.scss # Reusable code
├── base/
│ └── _reset.scss # Minimal, rarely changes
├── components/
│ ├── _button.scss # Independent modules
│ └── _card.scss # Can compile in parallel
└── main.scss # Thin orchestrator
// Benchmark:
// Monolithic: Change 1 variable → 2000ms recompile
// Modular: Change 1 variable → 400ms recompile (80% faster)
5. CSS Output Size Reduction Strategies
| Strategy | Implementation | Reduction | Notes |
|---|---|---|---|
| Use @extend | Share selectors | 10-30% | Careful with specificity |
| Placeholder Selectors | %silent classes | 15-25% | Not output unless extended |
| Avoid Deep Nesting | Flatten selectors | 5-15% | Better performance too |
| Compression | style=compressed | 30-40% | Essential for production |
| PurgeCSS | Remove unused CSS | 50-90% | Especially with frameworks |
| CSS Minification | PostCSS cssnano | 10-20% additional | After Sass compilation |
| Tree Shaking | @use instead of @import | 20-40% | Modern module system |
Example: Placeholder selectors for size optimization
// Using placeholder selectors (not output unless extended)
%card-base {
padding: 1rem;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
%flex-center {
display: flex;
align-items: center;
justify-content: center;
}
// These extend the placeholder (selector grouping in output)
.product-card {
@extend %card-base;
background: white;
}
.user-card {
@extend %card-base;
background: #f5f5f5;
}
.modal-header {
@extend %flex-center;
height: 60px;
}
// CSS Output (optimized):
.product-card, .user-card {
padding: 1rem;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.product-card { background: white; }
.user-card { background: #f5f5f5; }
.modal-header {
display: flex;
align-items: center;
justify-content: center;
height: 60px;
}
// Size comparison:
// Without placeholders: 280 bytes
// With placeholders: 185 bytes (34% smaller)
Example: PurgeCSS integration for unused CSS removal
// postcss.config.js
const purgecss = require('@fullhuman/postcss-purgecss');
module.exports = {
plugins: [
require('autoprefixer'),
// Only in production
...(process.env.NODE_ENV === 'production' ? [
purgecss({
content: [
'./src/**/*.html',
'./src/**/*.jsx',
'./src/**/*.tsx',
'./src/**/*.vue'
],
// Default extractors
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
// Safe list (never purge)
safelist: {
standard: ['active', 'disabled', 'hidden'],
deep: [/^modal-/, /^dropdown-/],
greedy: [/^data-/]
},
// Purge from variables too
variables: true,
// Keep keyframes if any animation uses them
keyframes: true
})
] : []),
require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true
}]
})
]
};
// package.json
{
"scripts": {
"build:css": "sass src/styles:dist/css && postcss dist/css/*.css --replace"
}
}
// Size reduction example:
// Before PurgeCSS: 450KB
// After PurgeCSS: 45KB (90% reduction)
Example: Complete optimization pipeline
// build-css.js - Comprehensive optimization
const sass = require('sass');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const purgecss = require('@fullhuman/postcss-purgecss');
const fs = require('fs').promises;
const path = require('path');
async function buildCSS() {
console.time('Total build time');
// Step 1: Compile Sass
console.time('Sass compilation');
const sassResult = sass.compile('src/styles/main.scss', {
style: 'expanded', // Will minify later for better optimization
sourceMap: false,
quietDeps: true
});
console.timeEnd('Sass compilation');
console.log(`Compiled size: ${(sassResult.css.length / 1024).toFixed(2)}KB`);
// Step 2: PostCSS transformations
console.time('PostCSS processing');
const plugins = [
autoprefixer(),
purgecss({
content: ['src/**/*.html', 'src/**/*.js'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
}),
cssnano({
preset: ['advanced', {
discardComments: { removeAll: true },
reduceIdents: true,
mergeRules: true,
mergeLonghand: true,
colormin: true,
normalizeWhitespace: true,
discardUnused: true,
minifyFontValues: true,
minifySelectors: true
}]
})
];
const result = await postcss(plugins).process(sassResult.css, {
from: 'main.scss',
to: 'main.css'
});
console.timeEnd('PostCSS processing');
// Step 3: Write output
await fs.writeFile('dist/main.css', result.css);
console.log(`Final size: ${(result.css.length / 1024).toFixed(2)}KB`);
console.log(`Reduction: ${((1 - result.css.length / sassResult.css.length) * 100).toFixed(1)}%`);
console.timeEnd('Total build time');
}
buildCSS().catch(console.error);
// Example output:
// Sass compilation: 245ms
// Compiled size: 450.32KB
// PostCSS processing: 892ms
// Final size: 42.18KB
// Reduction: 90.6%
// Total build time: 1.142s
6. Watch Mode and Live Reloading Setup
| Tool | Command | Features | Use Case |
|---|---|---|---|
| Sass CLI | sass --watch |
File watching, auto-compile | Simple projects |
| Vite | vite dev |
HMR, instant updates | Modern development |
| Webpack Dev Server | webpack serve |
HMR, proxy, history | Complex apps |
| Browser Sync | browsersync start | Multi-device sync | Cross-browser testing |
| Nodemon | nodemon --watch | Custom commands | Custom workflows |
Example: Sass CLI watch mode
// Basic watch mode
sass --watch src/scss:dist/css
// Watch with options
sass --watch \
--style=expanded \
--source-map \
--embed-sources \
src/scss:dist/css
// Watch specific file
sass --watch src/styles/main.scss:dist/css/main.css
// Watch with polling (for network drives, Docker)
sass --watch --poll src/scss:dist/css
// Multiple watch targets
sass --watch \
src/scss/app:dist/css/app \
src/scss/admin:dist/css/admin
// package.json scripts
{
"scripts": {
"watch": "sass --watch src/scss:dist/css",
"watch:dev": "sass --watch --style=expanded --source-map src/scss:dist/css",
"watch:prod": "sass --watch --style=compressed --no-source-map src/scss:dist/css"
}
}
Example: Vite with HMR (Hot Module Replacement)
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3000,
open: true,
// HMR configuration
hmr: {
overlay: true, // Show errors as overlay
clientPort: 3000
},
// Watch options
watch: {
usePolling: false, // Set true for Docker/WSL
interval: 100
}
},
css: {
devSourcemap: true,
preprocessorOptions: {
scss: {
// Inject global styles
additionalData: `@use "src/styles/globals" as *;`
}
}
}
});
// App component automatically reloads on SCSS changes
// Changes appear instantly without full page reload
// package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
Example: Browser-sync with Sass watching
// bs-config.js
module.exports = {
files: ['dist/**/*.css', 'src/**/*.html', 'src/**/*.js'],
server: {
baseDir: 'dist',
index: 'index.html'
},
port: 3000,
open: true,
notify: false,
// CSS injection (no page reload)
injectChanges: true,
// Reload delay
reloadDelay: 0,
// Multi-device sync
ghostMode: {
clicks: true,
forms: true,
scroll: true
}
};
// watch-and-serve.js
const { spawn } = require('child_process');
const browserSync = require('browser-sync').create();
// Start Sass watcher
const sassProcess = spawn('sass', [
'--watch',
'--style=expanded',
'src/scss:dist/css'
], { stdio: 'inherit' });
// Start BrowserSync
browserSync.init({
server: 'dist',
files: ['dist/**/*.css', 'src/**/*.html'],
port: 3000,
notify: false
});
// Cleanup on exit
process.on('SIGINT', () => {
sassProcess.kill();
browserSync.exit();
process.exit();
});
// package.json
{
"scripts": {
"dev": "node watch-and-serve.js"
},
"devDependencies": {
"browser-sync": "^2.29.3",
"sass": "^1.69.0"
}
}
Example: Custom watch script with live reload
// dev-server.js - Custom development server
const chokidar = require('chokidar');
const sass = require('sass');
const fs = require('fs').promises;
const path = require('path');
const { WebSocketServer } = require('ws');
const express = require('express');
const app = express();
const PORT = 3000;
// Serve static files
app.use(express.static('dist'));
// Inject live reload script
app.get('/', async (req, res) => {
let html = await fs.readFile('dist/index.html', 'utf-8');
html = html.replace('</body>', `
<script>
const ws = new WebSocket('ws://localhost:${PORT + 1}');
ws.onmessage = (msg) => {
if (msg.data === 'css-update') {
// Hot reload CSS without page refresh
const links = document.querySelectorAll('link[rel="stylesheet"]');
links.forEach(link => {
const href = link.href.split('?')[0];
link.href = href + '?t=' + Date.now();
});
} else if (msg.data === 'reload') {
location.reload();
}
};
</script>
</body>`);
res.send(html);
});
// WebSocket server for live reload
const wss = new WebSocketServer({ port: PORT + 1 });
const clients = new Set();
wss.on('connection', (ws) => {
clients.add(ws);
ws.on('close', () => clients.delete(ws));
});
function broadcast(message) {
clients.forEach(client => {
if (client.readyState === 1) client.send(message);
});
}
// Watch SCSS files
const scssWatcher = chokidar.watch('src/scss/**/*.scss', {
ignoreInitial: true
});
scssWatcher.on('change', async (filePath) => {
console.log(`SCSS changed: ${filePath}`);
try {
const result = sass.compile('src/scss/main.scss', {
style: 'expanded',
sourceMap: true
});
await fs.writeFile('dist/css/main.css', result.css);
console.log('CSS compiled successfully');
broadcast('css-update');
} catch (err) {
console.error('Sass compilation error:', err.message);
}
});
// Watch HTML files
const htmlWatcher = chokidar.watch('src/**/*.html', {
ignoreInitial: true
});
htmlWatcher.on('change', (filePath) => {
console.log(`HTML changed: ${filePath}`);
broadcast('reload');
});
// Start server
app.listen(PORT, () => {
console.log(`Dev server running at http://localhost:${PORT}`);
console.log('Watching for changes...');
});
Performance Optimization Summary
- Output styles: Use compressed for production, expanded for development
- Source maps: Enable in dev/staging, disable in production
- Modern tools: Vite offers best DX with zero-config and instant HMR
- @use over @import: 20-40% faster compilation with better scoping
- Caching: Enable filesystem cache for 50-80% faster rebuilds
- Size reduction: Combine placeholders, PurgeCSS (50-90% reduction)
- Watch mode: Use HMR for instant feedback without full reloads
- Build pipeline: Sass → PostCSS → Autoprefixer → PurgeCSS → Minification
Note: Modern compilers like Dart Sass are 2-3x faster than
deprecated node-sass. Always use the latest Sass version with the
modern-compiler API for best
performance.