Jak napisać własny system minifikacyjny z cache w szablonie Wordpress?

Jak napisać własny system minifikacyjny z cache w szablonie Wordpress?

WordPress daje nam dziesiątki gotowych wtyczek do minifikacji i cache’owania zasobów. Problem w tym, że w praktyce często robią za dużo, konfliktują się z motywem, wczytują własne skrypty albo psują kolejność ładowania plików.

Tworząc stronę WWW z własnym dedykowanym szablonem warto zadbać o pełną kontrolę nad CSS i JS.

W tym artykule pokażę krok po kroku, jak napisać własny, prosty i skuteczny system minifikacji z cache, bez wtyczek, bez kombinowania i bez zgadywania, co WordPress zrobi w tle. Wszystko osadzimy bezpośrednio w szablonie, dzięki czemu:

  • mamy pełną kontrolę nad kolejnością plików,

  • sami decydujemy, co i kiedy jest minifikowane,

  • cache działa dokładnie tak, jak tego oczekujemy,

  • a cały mechanizm jest lekki, przewidywalny i łatwy do debugowania.

 

Wpięcie minifiera do szablonu

Cały system minifikacji trzymamy poza functions.php, w osobnym katalogu. Dzięki temu:

  • logika minifikacji nie miesza się z resztą motywu,

  • łatwo ją rozwijać, testować i wyłączać,

  • zachowujemy porządek w strukturze projektu.

W functions.php dodajemy tylko jedno odwołanie:

require get_template_directory() . '/inc/minifier.php'; 

Katalog inc/ przeznaczamy na kod techniczny, a sam minifier będziemy dalej rozbijać na kolejne pliki, tak aby każda część (cache, CSS, JS) miała swoje miejsce.
Cała logika optymalizacji będzie zamknięta w jednym obszarze szablonu — bez chaosu i bez przypadkowych zależności.

 

Minifikacja CSS – pierwszy krok 🎯

Zaczynamy od najprostszego i w pełni kontrolowanego etapu, czyli minifikacji CSS. Nie korzystamy z zewnętrznych bibliotek ani parserów — to celowy wybór. Ten system ma być:

  • szybki,

  • przewidywalny,

  • łatwy do debugowania.

W pliku inc/minifier.php dodajemy funkcję odpowiedzialną wyłącznie za oczyszczanie CSS-a z nadmiarowych znaków:

function dcSMinifyCss(string $css){
    // Usuwamy komentarze CSS /* ... */
    $css = preg_replace('!/\*.*?\*/!s', '', $css);

    // Redukujemy wielokrotne puste linie
    $css = preg_replace('/\n\s*\n/', "\n", $css);

    // Usuwamy znaki nowej linii, tabulatory i CR
    $css = str_replace(["\r", "\n", "\t"], '', $css);

    // Redukujemy wielokrotne spacje do jednej
    $css = preg_replace('/\s+/', ' ', $css);

    // Usuwamy spacje wokół znaków składniowych
    $css = preg_replace('/\s*([{};:,])\s*/', '$1', $css);

    // Usuwamy zbędny średnik przed zamknięciem bloku
    $css = preg_replace('/;}/', '}', $css);

    return trim($css);
}

 

Dlaczego tak, a nie „pełna” minifikacja? 🧠

Ten mechanizm:

  • nie zmienia semantyki CSS,

  • nie próbuje „być mądrzejszy” od przeglądarki,

  • nie psuje niestandardowych hacków ani zmiennych CSS.

To jest bezpieczna minifikacja poziomu szablonu, idealna do:

  • własnych stylów,

  • kontrolowanych bibliotek,

  • plików generowanych przez nasz motyw.

 

Zbieranie i minifikacja plików CSS 🧩

Na tym etapie przechodzimy z „suchej” funkcji minifikującej do realnego mechanizmu budowania jednego pliku CSS z cache.
Całość zamykamy w jednej funkcji, która:

  • zbiera wszystkie style motywu,

  • sprawdza, czy cache jest aktualny,

  • w razie potrzeby regeneruje plik,

  • zwraca gotowy URL do podpięcia w <link>.

function dc_get_minified_css()
{
    $dc_assets_css   = get_template_directory() . '/cache/';
    $dc_file_css_name = 'dc-theme.min.css';
    $cache_file      = $dc_assets_css . $dc_file_css_name;

    // Lista CSS (lokalne + CDN)
    $files_css = array(
        'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css',
        get_template_directory() . '/style.css',
        get_template_directory() . '/assets/css/header.css',
        get_template_directory() . '/assets/css/header_baner.css',
        get_template_directory() . '/assets/css/header_contact.css',
        get_template_directory() . '/assets/css/header_menu.css',
        get_template_directory() . '/assets/css/header_menu_mobile.css',
        get_template_directory() . '/assets/css/header_search.css',
        get_template_directory() . '/assets/css/header_slidecart.css',
        get_template_directory() . '/assets/css/header_top.css',
        get_template_directory() . '/assets/css/owl.carousel.min.css',
        get_template_directory() . '/assets/css/products_carousel.css',
        get_template_directory() . '/assets/fontawesome/css/all.min.css',
        get_template_directory() . '/assets/css/forms.css',
        get_template_directory() . '/assets/css/footer.css',
        get_template_directory() . '/assets/css/product_card.css',
        get_template_directory() . '/assets/css/home.css',
        get_template_directory() . '/assets/css/woocommerce.css'
    );

    $regenerate = true;

 

Lista plików = pełna kontrola 🎛️

Nie polegamy na wp_enqueue_style.
Kolejność plików jest jawna i przewidywalna, co ma kluczowe znaczenie przy:

  • nadpisywaniu styli,

  • Bootstrapie,

  • WooCommerce,

  • komponentach UI.

CDN i pliki lokalne są obsługiwane razem, bez wyjątków.

 

Sprawdzanie aktualności cache ⏱️

    if ( file_exists( $cache_file ) ) {
        $cache_mtime = filemtime( $cache_file );
        $regenerate  = false;

        foreach ( $files_css as $file ) {

            if ( preg_match('#^https?://#', $file) ) {
                continue;
            }

            if ( file_exists($file) && filemtime($file) > $cache_mtime ) {
                $regenerate = true;
                break;
            }
        }
    }

Cache jest regenerowany tylko wtedy, gdy:

  • którykolwiek lokalny plik CSS został zmodyfikowany,

  • albo cache jeszcze nie istnieje.

Pliki z CDN celowo pomijamy przy sprawdzaniu czasu modyfikacji — traktujemy je jako stabilne zależności.

Efekt:

  • brak zbędnej minifikacji przy każdym requestcie 🚀

  • brak „zgadywania”, czy coś się zmieniło.

 

Budowanie zawartości CSS 🧱

        if ( function_exists('dcSMinifyCss') ) {
            $content = dcSMinifyCss($content);
        }

        @file_put_contents( $cache_file, $content );
    }

    return get_template_directory_uri() . '/cache/' . $dc_file_css_name;
}

Komentarze typu /* FILE */ i /* CDN */świadome:

  • ułatwiają debug,

  • pozwalają szybko zlokalizować problematyczny styl,

  • znikną później w procesie minifikacji.

 

Minifikacja i zapis do cache 🗜️

        if ( function_exists('dcSMinifyCss') ) {
            $content = dcSMinifyCss($content);
        }

        @file_put_contents( $cache_file, $content );
    }

    return get_template_directory_uri() . '/cache/' . $dc_file_css_name;
}

Minifikacja:

  • działa tylko na końcowym stringu,

  • nie dotyka pojedynczych plików,

  • jest szybka i bezpieczna.

Funkcja zwraca gotowy URL do pliku cache — bez warunków, bez logiki po stronie frontendu.

 

Teraz czas na pliki JavaScript ⚡

CSS mogliśmy minifikować prostymi regexami, bo CSS jest dość „wybaczający” i ma przewidywalną składnię (spacje, komentarze, średniki).
Z JavaScriptem tak się nie da.

 

Dlaczego nasza minifikacja CSS nie nadaje się do JS? 🚫

Minifikator CSS usuwa komentarze i spacje na ślepo. W JS to może rozwalić kod w kilka klasycznych miejsc:

  • // komentarz w JS kończy się końcem linii — jeśli wywalimy znaki nowej linii „hurtowo”, możemy skleić dwie instrukcje w jedną.

  • RegExp w JS (np. /\/*.../) potrafi wyglądać jak komentarz /* ... */ i regex minifikatora CSS może to błędnie wyciąć.

  • Spacje w JS czasem mają znaczenie semantyczne (np. return + nowa linia → pułapka ASI).

  • Łatwo też przypadkiem skleić tokeny w coś innego (a + +b, a - -b, itp.).

Dlatego do JS potrzebujemy narzędzia, które parsuje składnię, a nie „tnij-stringi”.

 

Dlaczego JShrink? ✅

JShrink to lekka biblioteka PHP, która minifikuje JavaScript w sposób bezpieczniejszy niż regexy.

Pobieramy z repo:
https://github.com/tedious/JShrink

Z repozytorium bierzemy plik:

  • Minifier.php

i wrzucamy do szablonu tutaj:

  • wp-content/themes/TWOJ_MOTYW/inc/JShrink/Minifier.php

 

Funkcja: sklejanie + cache + minifikacja JS (krok po kroku) 🧩

Poniższa funkcja buduje jeden plik dc-theme.min.js w katalogu cache/ i regeneruje go tylko wtedy, kiedy to ma sens.

 

1) Ustalamy ścieżkę cache i nazwę pliku

  • cache trzymamy w .../theme/cache/

  • wynik to zawsze dc-theme.min.js

 

2) Definiujemy listę plików JS (kolejność ma znaczenie)

To Ty decydujesz, co wchodzi do paczki i w jakiej kolejności. Najpierw CDN (Bootstrap), potem pliki motywu.

 

3) Sprawdzamy, czy cache jest aktualny

Jeśli cache istnieje:

  • bierzemy jego filemtime

  • porównujemy z czasami modyfikacji lokalnych plików JS

  • CDN pomijamy (nie mamy filemtime i traktujemy je jako zewnętrzne zależności)

Jeśli którykolwiek lokalny plik jest nowszy → przebudowujemy cache.

 

4) Sklejamy pliki w jeden string

Dla debugowania dodajesz nagłówki /* CDN: ... */ i /* FILE: ... */.
Potem i tak minifikacja to spłaszczy.

 

5) Ładujemy JShrink i minifikujemy całość

require_once wrzucasz wewnątrz regeneracji, dzięki czemu biblioteka ładuje się tylko wtedy, kiedy faktycznie przebudowujesz paczkę. 👍

 

6) Zapisujemy wynik do pliku i zwracamy URL

Funkcja na końcu zwraca gotowy URL do podpięcia w HTML.

 

Kompletny kod funkcji ✅

function dc_get_minified_js(){
    $dc_assets_js    = get_template_directory() . '/cache/';
    $dc_file_js_name = 'dc-theme.min.js';
    $cache_file      = $dc_assets_js . $dc_file_js_name;

    // Lista JS (lokalne + CDN)
    $files_js = array(
        'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js',
        get_template_directory() . '/assets/js/dc-slidecart.js',
        get_template_directory() . '/assets/js/woocommerce.js',
        get_template_directory() . '/assets/js/dc-theme.js',
        get_template_directory() . '/assets/js/custom.js'
    );

    $regenerate = true;

    if ( file_exists( $cache_file ) ) {
        $cache_mtime = filemtime( $cache_file );
        $regenerate  = false;

        foreach ( $files_js as $file ) {

            if ( preg_match('#^https?://#', $file) ) {
                continue;
            }

            if ( file_exists($file) && filemtime($file) > $cache_mtime ) {
                $regenerate = true;
                break;
            }
        }
    }

    if ( $regenerate ) {

        $content = '';

        foreach ( $files_js as $file ) {

            if ( preg_match('#^https?://#', $file) ) {

                $js = @file_get_contents($file);

                if ( $js !== false ) {
                    $content .= "\n/* CDN: $file */\n" . $js;
                }

            } elseif ( file_exists($file) ) {

                $js = @file_get_contents($file);

                if ( $js !== false ) {
                    $content .= "\n/* FILE: $file */\n" . $js;
                }
            }
        }

        // JShrink: pobrany z https://github.com/tedious/JShrink
        // Plik: /inc/JShrink/Minifier.php
        require_once(get_template_directory() . '/inc/JShrink/Minifier.php');

        // Minifikacja przez JShrink
        if ( class_exists('\JShrink\Minifier') ) {
            try {
                $content = \JShrink\Minifier::minify($content, [
                    'flaggedComments' => false
                ]);
            } catch (Exception $e) {
                // W razie błędu zapisujemy nieminifikowany JS
            }
        }

        @file_put_contents( $cache_file, $content );
    }

    return get_template_directory_uri() . '/cache/' . $dc_file_js_name;
}

 

Funkcja spinająca cały proces 🧩

To jest funkcja, która zamyka cały system. WordPress dostaje tylko jeden CSS i jeden JS — dokładnie te, które wcześniej wygenerowaliśmy.

function designcart_enqueue_assets(){
    wp_enqueue_style(
        'dc-theme-min-css',
        dc_get_minified_css(),
        [],
        filemtime( get_template_directory() . '/cache/dc-theme.min.css' )
    );

    wp_enqueue_script(
        'dc-theme-min-js',
        dc_get_minified_js(),
        [],
        filemtime( get_template_directory() . '/cache/dc-theme.min.js' )
    );
}
add_action( 'wp_enqueue_scripts', 'designcart_enqueue_assets' );

 

Co tu się faktycznie dzieje? 🧠

1) WordPress dostaje jeden plik CSS i jeden JS

Nie rejestrujemy dziesięciu styli i pięciu skryptów.
Dla frontendu istnieją tylko:

  • dc-theme-min.css

  • dc-theme-min.js

Mniej requestów = szybsze ładowanie 🚀

 

2) Funkcje dc_get_minified_css() i dc_get_minified_js()

Podczas enqueue:

  • jeśli cache jest aktualny → nic się nie przebudowuje,

  • jeśli coś się zmieniło → pliki są regenerowane przed załadowaniem.

WordPress nie wie, że w tle dzieje się minifikacja i cache — dostaje gotowy URL i koniec.

 

3) Wersjonowanie przez filemtime() 🔁

To bardzo ważny detal.

filemtime( get_template_directory() . '/cache/dc-theme.min.css' )

oraz:

filemtime( get_template_directory() . '/cache/dc-theme.min.js' )

Dzięki temu:

  • po każdej regeneracji cache zmienia się wersja pliku,

  • przeglądarka automatycznie pobiera nową wersję,

  • nie ma problemu z „wiszącym” starym CSS lub JS.

Zero ręcznego czyszczenia cache w przeglądarce 👌

 

Efekt końcowy 🎯

Po stronie WordPressa:

  • standardowe API (wp_enqueue_*)

  • zero hacków

  • pełna zgodność z core

Po stronie frontendu:

  • jeden CSS

  • jeden JS

  • cache na dysku

  • wersjonowanie automatyczne

Masz własny system minifikacji i cache w szablonie WordPress, bez wtyczek, bez konfliktów i bez zgadywania, co dzieje się „magicznie” w tle.

 

Kompletny kod pliku minifier.php

<?php

    function dcSMinifyCss(string $css){

        $css = preg_replace('!/\*.*?\*/!s', '', $css);
        $css = preg_replace('/\n\s*\n/', "\n", $css);
        $css = str_replace(["\r", "\n", "\t"], '', $css);
        $css = preg_replace('/\s+/', ' ', $css);
        $css = preg_replace('/\s*([{};:,])\s*/', '$1', $css);
        $css = preg_replace('/;}/', '}', $css);

        return trim($css);
    }

    function dc_get_minified_css() {

        $dc_assets_css = get_template_directory() . '/cache/';
        $dc_file_css_name = 'dc-theme.min.css';
        $cache_file = $dc_assets_css.$dc_file_css_name;

        // Lista CSS
        $files_css = array(
            'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css',
            get_template_directory() . '/style.css',
            get_template_directory() . '/assets/css/header.css',
            get_template_directory() . '/assets/css/header_baner.css',
            get_template_directory() . '/assets/css/header_contact.css',
            get_template_directory() . '/assets/css/header_menu.css',
            get_template_directory() . '/assets/css/header_menu_mobile.css',
            get_template_directory() . '/assets/css/header_search.css',
            get_template_directory() . '/assets/css/header_slidecart.css',
            get_template_directory() . '/assets/css/header_top.css',
            get_template_directory() . '/assets/css/owl.carousel.min.css',
            get_template_directory() . '/assets/css/products_carousel.css',
            get_template_directory() . '/assets/fontawesome/css/all.min.css',
            get_template_directory() . '/assets/css/forms.css',
            get_template_directory() . '/assets/css/footer.css',
            get_template_directory() . '/assets/css/product_card.css',
            get_template_directory() . '/assets/css/home.css',
            get_template_directory() . '/assets/css/woocommerce.css'
        );

        $regenerate = true;

        if ( file_exists( $cache_file ) ) {
            $cache_mtime = filemtime( $cache_file );
            $regenerate  = false;

            foreach ( $files_css as $file ) {

                if ( preg_match('#^https?://#', $file) ) {
                    continue;
                }

                if (file_exists($file) && filemtime($file) > $cache_mtime ) {
                    $regenerate = true;
                    break;
                }
            }
        }

        if ( $regenerate ) {

            $content = '';

            foreach ( $files_css as $file ) {

                if ( preg_match('#^https?://#', $file) ) {

                    $css = @file_get_contents($file);

                    if ( $css !== false ) {
                        $content .= "\n/* CDN: $file */\n" . $css;
                    }

                } elseif ( file_exists($file) ) {

                    $css = @file_get_contents($file);

                    if ( $css !== false ) {
                        $content .= "\n/* FILE: $file */\n" . $css;
                    }
                }
            }

            if ( function_exists('dcSMinifyCss') ) {
                $content = dcSMinifyCss($content);
            }

            @file_put_contents( $cache_file, $content );
        }

        return get_template_directory_uri() . '/cache/' . $dc_file_css_name;
    }

    function dc_get_minified_js(){
        $dc_assets_js    = get_template_directory() . '/cache/';
        $dc_file_js_name = 'dc-theme.min.js';
        $cache_file      = $dc_assets_js . $dc_file_js_name;

        // Lista JS (lokalne + CDN)
        $files_js = array(
            'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js',
            get_template_directory() . '/assets/js/dc-slidecart.js',
            get_template_directory() . '/assets/js/woocommerce.js',
            get_template_directory() . '/assets/js/dc-theme.js',
            get_template_directory() . '/assets/js/custom.js'
        );

        $regenerate = true;

        if ( file_exists( $cache_file ) ) {
            $cache_mtime = filemtime( $cache_file );
            $regenerate  = false;

            foreach ( $files_js as $file ) {

                if ( preg_match('#^https?://#', $file) ) {
                    continue;
                }

                if ( file_exists($file) && filemtime($file) > $cache_mtime ) {
                    $regenerate = true;
                    break;
                }
            }
        }

        if ( $regenerate ) {

            $content = '';

            foreach ( $files_js as $file ) {

                if ( preg_match('#^https?://#', $file) ) {

                    $js = @file_get_contents($file);

                    if ( $js !== false ) {
                        $content .= "\n/* CDN: $file */\n" . $js;
                    }

                } elseif ( file_exists($file) ) {

                    $js = @file_get_contents($file);

                    if ( $js !== false ) {
                        $content .= "\n/* FILE: $file */\n" . $js;
                    }
                }
            }
            require_once(get_template_directory().'/inc/JShrink/Minifier.php');
            // Minifikacja przez JShrink
            if ( class_exists('\JShrink\Minifier') ) {
                try {
                    $content = \JShrink\Minifier::minify($content, [
                        'flaggedComments' => false
                    ]);
                } catch (Exception $e) {
                    // W razie błędu zapisujemy nieminifikowany JS
                }
            }

            @file_put_contents( $cache_file, $content );
        }

        return get_template_directory_uri() . '/cache/' . $dc_file_js_name;
    }

    function designcart_enqueue_assets() {

        wp_enqueue_style(
            'dc-theme-min-css',
            dc_get_minified_css(),
            [],
            filemtime( get_template_directory() . '/cache/dc-theme.min.css' )
        );

        wp_enqueue_script(
            'dc-theme-min-js',
            dc_get_minified_js(),
            [],
            filemtime( get_template_directory() . '/cache/dc-theme.min.js' )
        );
    }
    add_action( 'wp_enqueue_scripts', 'designcart_enqueue_assets' );

 

Podsumowanie 🧠

Zbudowaliśmy własny, w pełni kontrolowany system minifikacji i cache bez użycia wtyczek i bez ingerowania w core WordPressa. Cała logika działa na poziomie szablonu, dokładnie tam, gdzie powinna — blisko kodu, który faktycznie kontrolujemy.

Ten mechanizm:

  • łączy wszystkie style w jeden plik CSS,

  • łączy wszystkie skrypty w jeden plik JS,

  • minifikuje CSS bezpiecznie prostą logiką,

  • minifikuje JS przy użyciu JShrink, czyli parsera, a nie regexów,

  • zapisuje wynik do cache na dysku,

  • regeneruje pliki tylko wtedy, gdy coś się zmieni,

  • automatycznie wersjonuje zasoby przez filemtime().

Efekt końcowy to:

  • mniej requestów HTTP,

  • szybsze ładowanie strony,

  • brak konfliktów z wtyczkami optymalizacyjnymi,

  • pełna kontrola nad kolejnością ładowania plików,

  • przewidywalne działanie w środowisku produkcyjnym.

To rozwiązanie ma największy sens w customowych motywach, gdzie wiemy, jakie pliki są ładowane i w jakiej kolejności. Nie próbuje być uniwersalne ani „magiczne” — robi dokładnie to, co ma robić, i nic więcej.

Zapoznaj się też z naszą ofertą tworzenia stron WWW nie tylko w Wordpress