Wie extrahiere ich kritisches CSS in Shopware 6?

Wie kann ich mithilfe von penthouse kritisches CSS für Shopware 6 extrahieren?

Wir gehen davon aus, dass wir uns im Projekt-Verzeichnis mit funktionierender Shopware-Installation befinden und Node.js sowie npm installiert sind.

  1. Initialisiere ein npm-Projekt im Theme-Verzeichnis, falls noch nicht geschehen.
    cd custom/plugins/FietzRevplusChild/src/Resources/app/storefront/
    
    npm init -y
    
  2. Penthouse installieren.
    npm i -D penthouse
    
  3. Penthouse konfigurieren.
    Erstelle eine Datei namens app/storefront/critical.config.js.
  4. Ausführungs-Script erstellen.
    Erstelle eine Datei namens app/storefront/src/critical.js.
  5. Script zur Generierung des kritischen CSS hinzufügen.
    Füge in der package.json folgendes zum Abschnitt scripts hinzu:
    "critical": "node ./src/critical.js"
    
  6. Critical CSS generieren.
    npm run critical
    

Konfiguration (critical.config.js)

Parameter für Konfigurationsdatei: critical.config.js
Parameter Typ Optional Standard Beschreibung
baseUrl string Pflicht 'http://127.0.0.1:8000' Die Basis-URL für die Anwendung.
shopwarePages object Pflicht s. unten URLs für Shopware-Seiten (Kategorie- und Produktseiten).
pages array Optional s. unten Seiten-Definitionen für die kritische CSS-Extraktion.
Zusätzliche Informationen
  • Es können beliebig viele Seiten zur Extraktion hinzugefügt werden, sie müssen lediglich in der pages-Definition aufgeführt werden und in shopwarePages referenziert werden.
/**
 * Critical CSS Konfiguration für Shopware 6 Theme
 *
 * Diese Konfiguration steuert die Extraktion von Critical CSS mit Penthouse.
 * Critical CSS enthält nur die Styles, die für den Above-the-Fold Bereich
 * einer Seite benötigt werden, um das initiale Rendering zu beschleunigen.
 *
 * Verwendung:
 * - `cd custom/plugins/FietzRevplus/src/Resources/app/storefront`
 * - `npm run critical` (führt die Extraktion aus)
 *
 * Generiert optimierte CSS-Dateien für verschiedene Seitentypen und Geräte.
 * Output: ./dist/critical/*.css
 *
 * Anpassung für eigene Projekte:
 * - baseUrl: Lokale/Staging URL der Shopware-Installation
 * - categoryUrl/productUrl: Beliebige existierende SEO-URLs aus dem Shop
 * - pages: Weitere Seitentypen hinzufügen oder Viewport-Größen anpassen
 */

module.exports = {
    baseUrl: 'http://127.0.0.1:8000',

    /**
     * Shopware Seiten-URLs für Kategorie- und Produktseiten. Diese URLs erset-
     * zen die Platzhalter in der `pages`-Definition.
     * 
     * @param categoryUrl Beliebige existierende Kategorie-URL
     * @param productUrl Beliebige existierende Produkt-URL
     */
    shopwarePages: {
        categoryUrl: '/Shoes/',
        productUrl: '/Aerodynamic-Bronze-Water-Whole/SW-019a0152f66d7131a5a1408e48eb997e',
    },

    /**
     * CSS-Selektoren, die immer in das Critical CSS aufgenommen werden sollen.
     * 
     * Nützlich für:
     * - Layout-kritische Styles zur Vermeidung von CLS (Cumulative Layout Shift)
     * - Container-Klassen, die das Layout definieren
     * - Klassen, die dynamisch erst nach dem initialen Rendering erscheinen
     * 
     * @see https://github.com/pocketjoso/penthouse?tab=readme-ov-file#options
     */
    forceInclude: [
        '.container',
        '.cms-section.boxed',
    ],

    pages: [
        {
            name: 'home-desktop',
            url: '/',
            width: 1920,
            height: 1080
        },
        {
            name: 'home-mobile',
            url: '/',
            width: 375,
            height: 667
        },
        {
            name: 'category-desktop',
            url: '/category-placeholder',
            width: 1920,
            height: 1080
        },
        {
            name: 'category-mobile',
            url: '/category-placeholder',
            width: 375,
            height: 667
        },
        {
            name: 'product-desktop',
            url: '/product-placeholder',
            width: 1920,
            height: 1080
        },
        {
            name: 'product-mobile',
            url: '/product-placeholder',
            width: 375,
            height: 667
        }
    ],

    penthouseOptions: {
        timeout: 30000,
        keepLargerMediaQueries: false,
        propertiesToRemove: [
            'transition', 'animation', 'transform', 'cursor'
        ]
    },

    outputDir: './dist/critical',
};

Ausführung (critical.js)

const penthouse = require('penthouse');
const fs = require('fs');
const path = require('path');
const config = require('../critical.config.js');

/**
 * Critical CSS Extraction für Shopware 6 Theme
 * 
 * Dieses Script extrahiert kritisches CSS mit Penthouse für verschiedene
 * Seitentypen und Viewport-Größen, um die initiale Ladezeit zu optimieren.
 */

/**
 * Sucht dynamisch nach der kompilierten all.css Datei im Shopware Theme-
 * Verzeichnis.
 * 
 * @returns {string|null} Pfad zur CSS-Datei oder null wenn nicht gefunden
 */
function findShopwareCss() {
    // Suche nach der all.css Datei im public/theme Verzeichnis
    const shopwareRoot = path.resolve(__dirname, '../../../../../../../../');
    const publicPath = path.join(shopwareRoot, 'public', 'theme');
    
    console.log('\nSuche CSS in:', publicPath);
    
    if (!fs.existsSync(publicPath)) {
        console.error('❌ Public theme Verzeichnis nicht gefunden:', publicPath);
        return null;
    }
    
    const themeDirs = fs.readdirSync(publicPath);
    
    for (const themeDir of themeDirs) {
        const cssPath = path.join(publicPath, themeDir, 'css', 'all.css');
        if (fs.existsSync(cssPath)) {
            console.log(`✅ CSS-Datei gefunden: ${themeDir}/css/all.css`);
            return cssPath;
        }
    }
    
    return null;
}

/**
 * Prüft, ob die CSS-Datei existiert und gibt den Pfad zurück.
 * Beendet das Script mit Fehlermeldung falls keine CSS-Datei gefunden wurde.
 * 
 * @returns {string} Pfad zur CSS-Datei
 */
function checkCssFile() {
    const cssPath = findShopwareCss();
    if (!cssPath) {
        console.error(`❌ Keine CSS-Datei gefunden`);
        console.log('💡 Führen Sie zuerst den Theme-Build aus');
        process.exit(1);
    }
    return cssPath;
}

/**
 * Stellt sicher, dass das Output-Verzeichnis für Critical CSS existiert.
 * Erstellt das Verzeichnis rekursiv falls es nicht vorhanden ist.
 */
function ensureOutputDir() {
    if (!fs.existsSync(config.outputDir)) {
        fs.mkdirSync(config.outputDir, { recursive: true });
    }
}

/**
 * Liest den Inhalt der CSS-Datei.
 * 
 * @param {string} cssPath - Pfad zur CSS-Datei
 * @returns {string} CSS-Inhalt als String
 */
function getCssContent(cssPath) {
    return fs.readFileSync(cssPath, 'utf8');
}

/**
 * Ersetzt Platzhalter-URLs durch echte Shopware-URLs aus der Config.
 * 
 * @param {string} url - URL mit möglichen Platzhaltern
 * @returns {string} Aufgelöste URL
 */
function resolveUrl(url) {
    return url
        .replace('/category-placeholder', config.shopwarePages.categoryUrl)
        .replace('/product-placeholder', config.shopwarePages.productUrl);
}

/**
 * Extrahiert Critical CSS für eine spezifische Seite.
 * 
 * @param {Object} pageConfig - Konfiguration der Seite (name, url, width, height)
 * @param {string} cssContent - Vollständiger CSS-Inhalt
 * @returns {Promise<Object|null>} Ergebnis-Objekt mit name, path und size oder null bei Fehler
 */
async function extractCriticalForPage(pageConfig, cssContent) {
    const resolvedUrl = resolveUrl(pageConfig.url);
    const fullUrl = `${config.baseUrl}${resolvedUrl}`;
    
    console.log(`🔍 Extrahiere: ${pageConfig.name} (${pageConfig.width}x${pageConfig.height})`);
    
    try {
        const criticalCss = await penthouse({
            url: fullUrl,
            cssString: cssContent,
            width: pageConfig.width,
            height: pageConfig.height,
            forceInclude: config.forceInclude || [],
            ...config.penthouseOptions
        });

        const outputPath = path.join(config.outputDir, `${pageConfig.name}.css`);
        fs.writeFileSync(outputPath, criticalCss);
        
        console.log(`   ✅ ${(criticalCss.length / 1024).toFixed(2)} KB\n`);
        
        return { name: pageConfig.name, path: outputPath, size: criticalCss.length };
    } catch (error) {
        console.error(`   ❌ Fehler: ${error.message}\n`);
        return null;
    }
}

/**
 * Erstellt eine kombinierte CSS-Datei aus allen extrahierten Critical CSS
 * Dateien.
 * 
 * @param {Array<Object>} results - Array mit Ergebnis-Objekten der Extraktion
 */
function createCombinedCss(results) {
    if (results.length === 0) return;
    
    const combinedCss = results.map(result => 
        fs.readFileSync(result.path, 'utf8')
    ).join('\n\n');
    
    const combinedPath = path.join(config.outputDir, 'combined.css');
    fs.writeFileSync(combinedPath, combinedCss);
    
    console.log(`✅ Kombinierte Datei: ${(combinedCss.length / 1024).toFixed(2)} KB`);
}

/**
 * Hauptfunktion: Orchestriert die gesamte Critical CSS Extraktion.
 * 
 * - Prüft CSS-Datei Existenz
 * - Erstellt Output-Verzeichnis
 * - Extrahiert Critical CSS für alle konfigurierten Seiten
 * - Erstellt kombinierte Datei
 */
async function generateCriticalCSS() {
    console.log('🚀 Starte Critical CSS Extraktion...');
    
    const cssPath = checkCssFile();
    ensureOutputDir();
    
    const cssContent = getCssContent(cssPath);
    console.log(`📄 CSS-Größe: ${(cssContent.length / 1024).toFixed(2)} KB\n`);
    
    const results = [];
    
    for (const pageConfig of config.pages) {
        const result = await extractCriticalForPage(pageConfig, cssContent);
        if (result) results.push(result);
        
        // Kurze Pause zwischen Requests
        await new Promise(resolve => setTimeout(resolve, 1000));
    }
    
    console.log(`📊 ${results.length} von ${config.pages.length} Dateien erstellt`);
    
    createCombinedCss(results);
    
    console.log('\n🎉 Critical CSS Extraktion abgeschlossen!');
}

// Startet den Extraktionsprozess
generateCriticalCSS().catch(error => {
    console.error('💥 Fehler:', error.message);
    process.exit(1);
});

Anpassung im Storefront-Template (Child-Theme)

Erweitere den Block layout_head_stylesheet der meta.html.twig-Datei des Child-Themes wie folgt, um das kritische CSS einzubinden:

{# FietzRevplusChild: Replace parent's critical CSS implementation with project specific styles. #}
{% block fietz_critical_css_include %}
    {# Change the filename here, if it differs from parent. #}
    {# % set criticalCssFile = pageType ~ '-' ~ deviceType ~ '.css' % #}
    {% set criticalCss = source('@FietzRevplusChild/app/storefront/dist/critical/' ~ criticalCssFile, ignore_missing = true) %}

    {# Fallback 1: Device only (home-mobile.css) #}
    {% if not criticalCss %}
        {% set fallbackFile = 'home-' ~ deviceType ~ '.css' %}
        {% set criticalCss = source('@FietzRevplusChild/app/storefront/dist/critical/' ~ fallbackFile, ignore_missing = true) %}
    {% endif %}

    {# Fallback 2: Combined file #}
    {% if not criticalCss %}
        {% set criticalCss = source('@FietzRevplusChild/app/storefront/dist/critical/combined.css', ignore_missing = true) %}
    {% endif %}

    {% if criticalCss %}
        <style>
            /* Critical CSS: {{ criticalCssFile }} ({{ deviceType }}, {{ pageType }}) */
            {{ criticalCss|raw }}
        </style>
    {% endif %}
{% endblock %}

Abhängigkeiten und eigene Individualisierung
Bitte beachte, dass die hier gezeigte Implementierung den Einsatz des RevPlus Themes ab Version v3.6.2 voraussetzt.
*Pflichtangaben Bitte beachten Sie unsere Datenschutzrichtlinien.
↓ Nehmen Sie jetzt Kontakt zu unserem Expertern auf ↓
Immo W. Fietz

Immo W. Fietz

eCommerce Berater seit 24 Jahren
(0 51 41) 204 892 0¹ support@fietz-medien.de
Sie haben noch mehr Fragen? Wir sind auch auf WhatsApp erreichbar!
f6b48afb-2514-428d-8ced-c658ef73d981
Karte mit Standort von FIETZ eCommerce
Kontakt
*Pflichtangaben Bitte beachten Sie unsere Datenschutzrichtlinien.
Direkter WhatsApp-Support