GZIP-Kompression für statische Dateien mit mod_rewrite und PHP

Dieses PHP-Skript hat sich seit seiner Erstveröffentlichung geändert. Zur neuesten Version des GZIP-Komprimierungs-Skripts auf github.

View english version of this article

Um Bandbreite zu sparen und die Downloadgeschwindigkeit zu erhöhen, lohnt es sich, auch statische Dateien wie Stylesheets und JavaScripts mit GZIP zu komprimieren und eine sinnvolle Cache-Verweildauer festzulegen. Das lässt sich mit einer angepassten .htacces-Datei gut automatisieren. Hierbei kann man entweder auf die dynamische Kompression mittels Apache mod_deflate oder aber eine Alternativlösung (in diesem Fall ein PHP-Skript) zurückgreifen. Ich habe mich für die PHP-Lösung entschieden, da ich die komprimierten Daten so im Dateisystem ablegen kann und etwas Prozessorleistung für die dynamische Kompression spare, die sonst mit mod_deflate bei jedem Dateizugriff erfolgt (und nicht nur beim ersten oder bei Änderungen, was den Vorteil der PHP-Lösung ausmacht).

Das PHP-Skript, das sich um die Kompression der angefragten Dateien kümmert und sie als statische .gz-Dateien auf dem Server ablegt, sieht wie folgt aus:

PHP-Skript

<?php

function get_content_type($file) {
    // Determine Content-Type based on file extension
    // Default to text/html
    $info = pathinfo($file);
    $content_types = array('css' => 'text/css; charset=UTF-8',
                           'html' => 'text/html; charset=UTF-8',
                           'gif' => 'image/gif',
                           'ico' => 'image/x-icon',
                           'jpg' => 'image/jpeg',
                           'jpeg' => 'image/jpeg',
                           'js' => 'application/javascript',
                           'json' => 'application/json',
                           'png' => 'image/png',
                           'txt' => 'text/plain',
                           'xml' => 'application/xml');
    if (empty($content_types[$info['extension']]))
        return 'text/html; charset=UTF-8';
    return $content_types[$info['extension']];
}

function main() {
    // Get file path by stripping query parameters from the request URI
    if (!empty($_SERVER['REQUEST_URI']))
        $path = preg_replace('/\/?(?:\?.*)?$/', '', $_SERVER['REQUEST_URI']);

    // If the path is empty, either use DEFAULT_FILENAME if defined, or exit
    if (empty($path)) {
        if (defined('DEFAULT_FILENAME')) $path = '/' . DEFAULT_FILENAME;
        else die();
    }

    $file = dirname(__FILE__) . $path;
    if (!file_exists($file)) die();

    $mtime = filemtime($file);

    // If the user agent sent a IF_MODIFIED_SINCE header, check if the file
    // has been modified. If it hasn't, send '304 Not Modified' header & exit
    if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
        $mtime <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
        header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified', true, 304);
        exit;
    }
    
    // Determine Content-Type based on file extension
    $content_type = get_content_type($file);

    // If the user agent accepts GZIP encoding, store a compressed version of
    // the file (<filename>.gz)
    if (!empty($_SERVER['HTTP_ACCEPT_ENCODING']) &&
        in_array('gzip', preg_split('/\s*,\s*/',
                                    $_SERVER['HTTP_ACCEPT_ENCODING']))) {
        // Only write the compressed version if it does not yet exist or the
        // original file has changed
        $gzfile = $file . '.gz';
        if (!file_exists($gzfile) || filemtime($gzfile) < $mtime)
            file_put_contents($gzfile, gzencode(file_get_contents($file)));
        // Send compression headers and use the .gz file instead of the
        // original filename
        header('Content-Encoding: gzip');
        $file = $file . '.gz';
    }

    // Vary max-age and expiration headers based on content type
    switch ($content_type) {
        case 'image/gif':
        case 'image/jpeg':
        case 'image/png':
            // Max-age for images: 31 days
            $maxage = 60 * 60 * 24 * 31;
            break;
        default:
            // Max-age for everything else: 7 days
            $maxage = 60 * 60 * 24 * 7;
    }

    // Send remaining headers
    header('Vary: Accept-Encoding');
    header('Cache-Control: max-age=' . $maxage);
    header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $maxage) . ' GMT');
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
    header('Content-Type: ' . $content_type);
    header('Content-Length: ' . filesize($file));
    
    // If the request method isn't HEAD, send the file contents
    if ($_SERVER['REQUEST_METHOD'] != 'HEAD') readfile($file);
}

main();

?>

Das Skript sollte als ‚gz.php‘ im Wurzelverzeichnis der WordPress-Installation liegen. Es erkennt, wann eine Datei geändert wurde und komprimiert diese nur dann erneut. Ausserdem sendet es eine ‚304 Not Modified‘-Antwort, falls eine angefragte Datei seit der letzten Anfrage nicht verändert wurde.

Dann benötigen wir eine angepasste .htaccess-Datei. Für meine WordPress-Installation sieht diese wie folgt aus:

.htaccess-Datei

# Set 'Vary' response header for .gz files
<FilesMatch "\.gz$">
<IfModule mod_headers.c>
Header always append Vary "Accept-Encoding"
</IfModule>
</FilesMatch>

<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType application/json "access plus 1 week"
ExpiresByType application/x-javascript "access plus 1 week"
ExpiresByType application/xml "access plus 1 week"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/x-icon "access plus 1 month"
ExpiresByType text/css "access plus 1 week"
ExpiresByType text/html "access plus 1 week"
ExpiresByType text/plain "access plus 1 week"
</IfModule>

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

# BEGIN GZIP
<IfModule mod_rewrite.c>
# If the user agent accepts gzip encoding...
RewriteCond %{HTTP:Accept-Encoding} gzip
# ...and the requested file exists...
RewriteCond %{REQUEST_FILENAME} -f
# ...then use a PHP script serve a compressed version. Done.
RewriteRule \.(css|html|ico|js|json|txt|xml)$ /gz.php [L]
</IfModule>
# END GZIP

Für .gz-Dateizugriffe wird der ‚Vary‘-Header festgelegt, um korrektes Caching über Proxy-Server zu gewährleisten. Dann folgen einige Direktiven zur Cache-Verweildauer (‚Expires‘- bzw. ‚Cache-Control‘-Header), diese lassen sich nach eigenem Gusto anpassen. Im Beispiel laufen CSS- und JS-Dateien nach einer Woche im Cache ab, Bilddateien nach einem Monat.

Alternativ lässt sich die mod_rewrite-Direktive für das PHP-Skript auch so ändern, dass nur der erste Zugriff einen Aufruf des Skripts erzeugt, und danach direkt die erstellte .gz-Datei abgerufen wird. Dazu die Zeilen zwischen # BEGIN GZIP und # END GZIP in dem .htaccess-Beispiel oben durch folgende Zeilen ersetzen:

<IfModule mod_rewrite.c>
# If the user agent accepts gzip encoding...
RewriteCond %{HTTP:Accept-Encoding} gzip
# ...and if gzip-encoded version of the requested file exists (<file>.gz)...
RewriteCond %{REQUEST_FILENAME}.gz -f
# ...then serve the gzip-encoded file. Done.
RewriteRule ^(.+)$ $1.gz [L]
# Or if the user agent accepts gzip encoding...
RewriteCond %{HTTP:Accept-Encoding} gzip
# ...and the requested file exists...
RewriteCond %{REQUEST_FILENAME} -f
# ...then use a PHP script serve a compressed version. Done.
RewriteRule \.(css|html|ico|js|json|txt|xml)$ /gz.php [L]
</IfModule>

Bildnachweis: .gz-Icon basierend auf generischem Paket-Icon aus GNOME 2.18 Icon Theme, veröffentlicht unter GNU GPL 2.0 Lizenz

Creative Commons Lizenzvertrag Dieser Beitrag wurde unter Creative Commons Namensnennung-Nicht-kommerziell-Weitergabe unter gleichen Bedingungen 3.0 Deutschland Lizenz veröffentlicht.

4 Antworten auf “GZIP-Kompression für statische Dateien mit mod_rewrite und PHP”

  • Hallo

    kurze Frage: Ich bin bei Hosteurope und mir steht nur „mod_rewrite“ im Basic Paket zur Verfügung. Reicht das aus, um das hier zu verwirklichen?

    • Hi, solange PHP auch verfügbar ist (+Zlib, ist aber meistens aktiviert), sollte es funktionieren. Scheint bei Hosteurope Basic der Fall zu sein.

  • Gzip is great for compressing files but don’t think that it’ll aaylws be the best option for you. Gzip adds processing overhead to the server, specially since these pages aren’t cached and are gzipped on the fly. As bandwidth gets cheaper, it really doesn’t make sense to sacrifice precious server processing time on gzipping. I would recommend caching gzipped pages and using the htaccess to serve those if they exist in a directory (such as /cache )

    • Gzip adds processing overhead to the server, specially since these pages aren’t cached and are gzipped on the fly.

      Yes, but this PHP solution works differently. It writes out the actual compressed data stream to a file on the filesystem, and this file will then be served. So the overhead is only the first time a file is accessed, or when it’s changed (in which case the compressed version will be re-generated).

      I would recommend caching gzipped pages and using the htaccess to serve those

      That’s exactly what this solution is doing (although it doesn’t use a cache directory, the files are simply stored next to the original with .gz extension).