Webpack, Webpack Encore, Turbo — the Symfony ecosystem has recommended various build tools over the years. For my projects, I use none of them. Instead: three lean Node.js scripts that do exactly what is needed.
Why Not Webpack?
Webpack is a powerful tool — but for projects that primarily need to compile SCSS and copy a few vendor files, it is overkill. The configuration is complex, the build times are long, and every update carries the risk of breaking changes in the plugin chain.
My approach: Three small scripts, each under 50 lines, each for one task.
Step 1: Compile SCSS
import * as sass from 'sass';
import fs from 'fs';
import path from 'path';
const cssDir = path.join(process.cwd(), 'public/assets/css');
const cssOut = path.join(cssDir, 'all.css');
const scssFile = path.join(
process.cwd(),
'templates/assets/scss/all.scss'
);
if (!fs.existsSync(cssDir)) {
fs.mkdirSync(cssDir, { recursive: true });
}
const result = sass.compile(scssFile, {
style: 'compressed',
loadPaths: [
path.join(process.cwd(), 'node_modules/bootstrap/scss')
]
});
fs.writeFileSync(cssOut, result.css, 'utf8');
This compiles the entire SCSS chain — custom variables, Bootstrap 5 with custom overrides, components, pages — into a single, minified CSS file.
Step 2: Copy Assets
import fse from 'fs-extra';
const copies = [
{ from: 'templates/assets/img', to: 'public/assets/img' },
{ from: 'templates/assets/fonts', to: 'public/assets/fonts' },
{
from: 'node_modules/bootstrap/dist/js/bootstrap.min.js',
to: 'public/assets/js/vendor/bootstrap.min.js'
},
];
for (const { from, to } of copies) {
await fse.copy(from, to, { overwrite: true });
}
Simple copying — images, fonts, vendor JavaScript. No transformations, no loaders, no plugins.
Step 3: Compression
import { readFile, writeFile } from 'fs/promises';
import { gzipSync, brotliCompressSync } from 'zlib';
import { glob } from 'glob';
const files = await glob('public/assets/**/*.{css,js}');
for (const file of files) {
const content = await readFile(file);
// Gzip
const gzipped = gzipSync(content, { level: 9 });
await writeFile(file + '.gz', gzipped);
// Brotli
const brotli = brotliCompressSync(content);
await writeFile(file + '.br', brotli);
}
nginx serves the pre-compressed files directly with brotli_static on and gzip_static on — without having to compress at runtime.
Bootstrap 5 with Custom Variables
The core of the SCSS setup: Define custom variables before the Bootstrap import so that Bootstrap picks up the overrides:
// _variables.scss — import BEFORE Bootstrap
$primary: #1a365d;
$accent: #3182ce;
$font-family-sans-serif: 'Inter', -apple-system, sans-serif;
$border-radius: 0.5rem;
// all.scss
@import "fonts";
@import "variables";
@import "../../../node_modules/bootstrap/scss/bootstrap";
@import "_components/navbar";
@import "_components/hero";
@import "_pages/home";
@import "_pages/blog";
The result: A single CSS file with Bootstrap base and project-specific styles, compressed to under 50 KB (Brotli).
Self-Hosted Fonts
Loading Google Fonts externally is a GDPR issue — every page view sends the visitor's IP address to Google. The solution: Download fonts and serve them locally.
// _fonts.scss
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400 800;
font-display: swap;
src: url('../fonts/inter-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
font-weight: 400 800 uses variable font technology — a single file for all weights instead of five separate files.
Build Command
{
"scripts": {
"build": "node build-scss.js && node copy-assets.js && node compress.js"
}
}
Build time: under 3 seconds. For comparison: Webpack Encore takes 15-20 seconds for a comparable setup.
When to Use Vite Instead?
For projects with reactive JavaScript components (Svelte, Vue), I use Vite. The SCSS compilation still stays in the Node scripts — Vite only handles the TypeScript/Svelte part. This separation keeps complexity low and each tool does exactly one thing.
Conclusion
Not every project needs a complex bundler. For Symfony sites with Bootstrap and SCSS, three Node scripts are perfectly sufficient. The build pipeline is transparent, fast, and has no hidden configuration layers. When something doesn't work, you open a 40-line script instead of a 200-line Webpack config.
Kommentare
Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.