Zum Inhalt springen

SCSS + Bootstrap 5 Build Pipeline Without Webpack

Veröffentlicht am Feb 7, 2026 | ca. 3 Min. Lesezeit |

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.

Thomas Wunner

Thomas Wunner

Certified IT specialist for application development with an instructor qualification and over 14 years of experience building scalable web applications with Symfony and Shopware. When not coding, Thomas volunteers as a lifeguard with the Wasserwacht, performs as a DJ, and explores the countryside on his motorbike.

Kommentare

Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.