Zum Inhalt springen

SCSS + Bootstrap 5 Build-Pipeline ohne Webpack

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

Webpack, Webpack Encore, Turbo — das Symfony-Ökosystem hat über die Jahre verschiedene Build-Tools empfohlen. Für meine Projekte nutze ich keines davon. Stattdessen: drei schlanke Node.js-Skripte, die genau das tun, was gebraucht wird.

Warum kein Webpack?

Webpack ist ein mächtiges Tool — aber für Projekte, die primär SCSS kompilieren und ein paar Vendor-Dateien kopieren müssen, ist es Overkill. Die Konfiguration ist komplex, die Build-Zeiten lang, und jedes Update birgt das Risiko von Breaking Changes in der Plugin-Kette.

Mein Ansatz: Drei kleine Scripts, jedes unter 50 Zeilen, jedes für eine Aufgabe.

Schritt 1: SCSS kompilieren

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');

Das kompiliert die komplette SCSS-Kette — eigene Variablen, Bootstrap 5 mit Custom-Overrides, Komponenten, Seiten — in eine einzige, minifizierte CSS-Datei.

Schritt 2: Assets kopieren

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 });
}

Einfaches Kopieren — Bilder, Fonts, Vendor-JavaScript. Keine Transformationen, keine Loader, keine Plugins.

Schritt 3: Komprimierung

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 serviert die pre-komprimierten Dateien direkt mit brotli_static on und gzip_static on — ohne zur Laufzeit komprimieren zu müssen.

Bootstrap 5 mit Custom Variables

Der Kern des SCSS-Setups: Eigene Variablen vor dem Bootstrap-Import definieren, damit Bootstrap die Overrides übernimmt:

// _variables.scss — VOR Bootstrap importieren
$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";

Das Ergebnis: Eine einzige CSS-Datei mit Bootstrap-Basis und projektspezifischen Styles, komprimiert auf unter 50 KB (Brotli).

Self-Hosted Fonts

Google Fonts extern zu laden ist ein DSGVO-Problem — jeder Seitenaufruf sendet die IP-Adresse des Besuchers an Google. Die Lösung: Fonts herunterladen und lokal servieren.

// _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 nutzt die Variable-Font-Technologie — eine einzige Datei für alle Gewichtungen, statt fünf separate Dateien.

Build-Befehl

{
    "scripts": {
        "build": "node build-scss.js && node copy-assets.js && node compress.js"
    }
}

Build-Zeit: unter 3 Sekunden. Zum Vergleich: Webpack Encore braucht für ein vergleichbares Setup 15-20 Sekunden.

Wann doch Vite?

Für Projekte mit reaktiven JavaScript-Komponenten (Svelte, Vue) nutze ich Vite. Die SCSS-Kompilierung bleibt trotzdem in den Node-Scripts — Vite übernimmt nur den TypeScript/Svelte-Teil. Diese Trennung hält die Komplexität gering und jedes Tool macht genau eine Sache.

Fazit

Nicht jedes Projekt braucht einen komplexen Bundler. Für Symfony-Seiten mit Bootstrap und SCSS reichen drei Node-Scripts völlig aus. Die Build-Pipeline ist transparent, schnell und hat keine versteckten Konfigurationsschichten. Wenn etwas nicht funktioniert, öffnen Sie ein 40-Zeilen-Script statt eines 200-Zeilen-Webpack-Configs.

Thomas Wunner

Thomas Wunner

Fachinformatiker für Anwendungsentwicklung mit Ausbildereignungsprüfung und über 14 Jahre Erfahrung im Aufbau skalierbarer Webanwendungen mit Symfony und Shopware. Abseits der Tastatur ist Thomas als Rettungsschwimmer in der Wasserwacht aktiv, legt als DJ auf und erkundet die Umgebung auf dem Motorrad.

Kommentare

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