Jörg Reinholz: Das Skript zum Sonntag; Minifiziertes und gezipptes CSS senden

Moin!

CSS-Files werden manchmal so groß, dass man diese erst minifizieren und dann auch noch kuhzippen will. Auch sollen die Browser diese cachen, wozu es ggf. spezielle HTTP-Header braucht.

Natürlich wäre es unschön, wenn die Datei bei jedem Abruf minifiziert und gepackt wird ... also werden diese Versionen (und ein Etag für die noch zu schreibende Cache-Optimierung) gespeichert.

Das folgende PHP-Skript wird dieses automatisch einrichten. Man muss nur nach einer Veränderung entweder die minifizierte Version (z.B. style.css.min) oder die gezippte Version style.css.min.gz oder die Datei mit dem ETag löschen. Oder aber css_packer.php?pack=style.css&renew=1 aufrufen.

Erst einmal ein paar Voreinstellungen:

## file: php.ini (im gleichen Verzeichnis wie css_packer.php)
allow_url_fopen = Off

Damit man nicht viele Seiten ändern muss ist dieses für den Apache hoffentlich hilfreich:

## file: .htaccess (im gleichen Verzeichnis wie css_packer.php)
RewriteEngine on
RewriteRule   ^(.*\.css)$ css_packer.php?pack=$1

Eine Readme fürs Erinnern:

## file: css_packer.readme (im gleichen Verzeichnis wie css_packer.php)
Benutzung:
Rufen Sie dieses Skript statt der CSS-Datei auf.

Automatisch geht dieses (in vielen aber nicht allen Fällen) mit einer Umleitung in einer Datei .htaccess :

Beispiel:
RewriteEngine on
RewriteRule   ^(.*\.css)$ css_packer.php?pack=$1

Es ist aber möglich,  css_packer.php?pack=style.css direkt anzugeben.

Hinweise:

Die packcss.php und die .htaccess muss im gleichen Verzeichnis wie die CSS-Datei sein!
Die CSS-Datei MUSS die Endung '.css' haben!

Bei einem Aufruf mit css_packer.php?renew=1&pack=style.css werden die gepackten und gezippten Dateien und das Etag-File erneuert.
Es muss nur die wirklich menschenlesbare Version manuell gepflegt werden...

Und dann das Skript:

<?php
/** file: css_packer.php
Autor: Jörg Reinholz, www.fastix.org
Lizenz: GPL 2.0 http://www.gnu.org/licenses/old-licenses/gpl-2.0.de.html **/

/** Konfiguration:
Wie lange soll der Cache gültig sein? **/
define ('CACHE_DAYS', 7); #Tage

/** Programm: **/

define ('MINIMAL_CSS'            , './' . $_GET['pack'] . '.min');
define ('MINIMAL_CSS_ZIPPED'     , './' . $_GET['pack'] . '.min.gz');
define ('ETAG'                   , './' . $_GET['pack'] . '.etag');

if ( empty($_GET['pack']) )  {
    error_log('Fehler: Keine Datei übergeben. (?pack=)');
    echo "Fehler: Keine Datei übergeben.\n\n";
    include('pack_css.readme');
    exit;
}

if ( substr($_GET['pack'],-4) != '.css' ) {
    error_log('Hinweis: Netter Versuch: (?pack=' . $_GET['pack'] . ')');
    echo "Netter Versuch!\n\n";
    include('pack_css.readme');
    exit;
}

if ( strpos($_GET['pack'], '..') ) {
    error_log('Hinweis: Netter Versuch. (?pack=' . $_GET['pack'].')');
    echo "Netter Versuch!\n\n";
    include('pack_css.readme');
    exit;
}

$_GET['pack']='./' . $_GET['pack'];

if (! empty($_GET['renew']) && $_GET['renew']) {
    // Datei(en) nicht vorhanden? Egal!
    @unlink(MINIMAL_CSS);
    @unlink(MINIMAL_CSS_ZIPPED);
    @unlink(ETAG);
}

if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
        $_SERVER['HTTP_ACCEPT_ENCODING']=false;
}
if (
    (! is_file(MINIMAL_CSS) )
or
    (! is_file(MINIMAL_CSS_ZIPPED) )
or
    (! is_file(ETAG) )
) {
    if ( ! is_file ($_GET['pack']) || ! is_readable ($_GET['pack']) ) {
        die('Fehler: Die CSS-Datei ' . $_GET['pack'] . ' gibt es nicht.');
    }
    if ( ! is_writable (__DIR__) ) {
        error_log('In das Verzeichnis ' . __DIR__ . ' kann nicht geschrieben werden.');
        die('Fehler: In das Verzeichnis kann nicht geschrieben werden.');
    }

    umask(0022);
    $file = file_get_contents($_GET['pack'], NULL, NULL);
    $file = str_replace(array("\n", "\r", "\t"),' ',$file);
    $file = preg_replace('/ {2, }/', ' ', $file);
    $file = str_replace(array(', ',' ,','; ',' ;','{ ',' {','} ',' }',': ',' :'), array(',',',',';',';','{','{','}','}',':',':'), $file);
    file_put_contents(MINIMAL_CSS, $file);
    file_put_contents(MINIMAL_CSS_ZIPPED, gzencode($file, 9));
    file_put_contents(ETAG, dechex(crc32($file)));
}

$max_age = CACHE_DAYS * 86400;
$exp_gmt = gmdate("D, d M Y H:i:s", time() + $max_age) . ' GMT';
$mod_gmt = gmdate("D, d M Y H:i:s",  filemtime($_GET['pack']) ) . ' GMT';

if (false===strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) {
    header ('Content-Type:text/css');
    header ("Expires:$exp_gmt");
    header ('Cache-Control:public');
    header ("Cache-Control: max-age=$max_age, public");
    header ("Last-Modified:$mod_gmt");
    header ('Etag: "' . file_get_contents(ETAG) . '"');
    readfile (MINIMAL_CSS);
    exit;
} else {
    header ('Content-Type:text/css');
    header ("Expires:$exp_gmt");
    header ('Cache-Control:public');
    header ("Cache-Control: max-age=$max_age, public");
    header ("Last-Modified:$mod_gmt");
    header ('Etag: "' . file_get_contents(ETAG) . '"');
    header ('Vary:Accept-Encoding');
    header ('Content-Encoding:gzip');
    readfile (MINIMAL_CSS_ZIPPED);
    exit;
}

Falls sich jemand aufregen will: Das ist meine Schönschrift. Und das Skript läuft auf fastix.org.

Jörg Reinholz

  1. Moin!

    Download aller Dateien:

    http://fastix.org/r/css_packer.tar.gz

    Jörg Reinholz

    1. Moin!

      Angeregt durch das Lob habe ich eine Version 2 herausgebracht, die jetzt auch den Status 304 (not modified) beherrscht und also bei übereinstimmenden ETag nicht mal versucht, die Daten zu senden.

      Die Adresse bleibt die alte:

      http://fastix.org/r/css_packer.tar.gz

      Es bleibt auch (mit Kommentaren und jetzt besser aufgeräumten, menschenlesbaren Code) beim schlanken "UHU" ("unter-100-Zeiler"):

      <?php
      /**
          css_packer.php Version 2.0.2
          Autor: Jörg Reinholz, www.fastix.org
          Lizenz: GPL 2.0 http://www.gnu.org/licenses/old-licenses/gpl-2.0.de.html
      **/
      
      /* Konfiguration: */
      # Wie lange soll der Cache gültig sein?
      define ('CACHE_DAYS', 7); #Tage
      
      /* Programm: */
      if ( empty($_GET['pack']) )  {
          error_log('Fehler: Keine Datei übergeben. (?pack=)');
          echo "Fehler: Keine Datei übergeben.\n\n";
          include('css_packer.readme');
          exit;
      }
      
      /* Sicherheits-Voodoo: */
      $_GET['pack'] = './' . $_GET['pack'];
      
      define ('MINIMAL_CSS'            , $_GET['pack'] . '.min');
      define ('MINIMAL_CSS_ZIPPED'     , $_GET['pack'] . '.min.gz');
      define ('ETAG'                   , $_GET['pack'] . '.etag');
      
      if ( substr($_GET['pack'],-4) != '.css' ) {
          error_log('Hinweis: Netter Versuch: (?pack=' . $_GET['pack'] . ')');
          echo "Netter Versuch!\n\n";
          include('css_packer.readme');
          exit;
      }
      if ( strpos($_GET['pack'], '..') ) {
          error_log('Hinweis: Netter Versuch. (?pack=' . $_GET['pack'] . ')');
          echo "Netter Versuch!\n\n";
          include('css_packer.readme');
          exit;
      }
      
      if (
              ! is_file(MINIMAL_CSS)
              or ! is_file(MINIMAL_CSS_ZIPPED)
              or ! is_file(ETAG)
              or ! empty($_GET['renew'])
          ) {
          if ( ! is_file ($_GET['pack']) || ! is_readable ($_GET['pack']) ) {
              error_log ('Fehler: Die CSS-Datei ' . $_GET['pack'] . ' gibt es nicht oder ist nicht lesbar.');
              echo 'Fehler: Die CSS-Datei gibt es nicht oder ist nicht lesbar.';
              exit;
          }
          if ( ! is_writable (__DIR__) ) {
              error_log('In das Verzeichnis ' . __DIR__ . ' kann nicht geschrieben werden.');
              echo 'Fehler: In das Verzeichnis kann nicht geschrieben werden.';
              exit;
          }
          @unlink(MINIMAL_CSS);
          @unlink(MINIMAL_CSS_ZIPPED);
          @unlink(ETAG);
          $data = file_get_contents($_GET['pack'], NULL, NULL);
          $data = str_replace(array("\n", "\r", "\t"),' ',$data);
          $data = preg_replace('/ {2,}/', ' ', $data);
          $data = str_replace(
              array( ', ', ' ,', '; ', ' ;', '{ ', ' {', '} ', ' }', ': ', ' :' ),
              array( ',',  ',',  ';',  ';',  '{',  '{',  '}',  '}',  ':',  ':' ),
              $data
          );
          umask(0022);
          file_put_contents(MINIMAL_CSS, $data);
          $data_gz = gzencode($data, 9);   file_put_contents(MINIMAL_CSS_ZIPPED, $data_gz);
          $etag    = dechex(crc32($data)); file_put_contents(ETAG, $etag);
      }
      
      $max_age = CACHE_DAYS * 86400;
      $exp_gmt = gmdate("D, d M Y H:i:s", time() + $max_age) . ' GMT';
      $mod_gmt = gmdate("D, d M Y H:i:s",  filemtime($_GET['pack']) ) . ' GMT';
      
      /* Senden */
      $etag = file_get_contents(ETAG);
      header ("Etag:$etag");
      if ( empty ( $_SERVER['HTTP_IF_NONE_MATCH']) || $_SERVER['HTTP_IF_NONE_MATCH'] != $etag ) {
          header ('Content-Type:text/css');
          header ("Expires:$exp_gmt");
          header ("Cache-Control:max-age=$max_age, public");
          header ("Last-Modified:$mod_gmt");
          /* gezippt ? */
          if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
                  $_SERVER['HTTP_ACCEPT_ENCODING'] = false;
          }
          if (false === strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) {
              if ( empty($data) ) {readfile(MINIMAL_CSS);} else {echo $data;}
          } else {
              header ('Vary:Accept-Encoding');
              header ('Content-Encoding:gzip');
              if ( empty($data_gz) ) {readfile(MINIMAL_CSS_ZIPPED);} else {echo $data_gz;}
          }
      } else {
          header('Status: 304');
          echo '';
      }
      

      Jörg Reinholz

  2. Servus!

    Vielen Dank, habe es im Wiki verlinkt: PHP/Anwendung und Praxis#Forum-Threads

    Herzliche Grüße

    Matthias Scharwies

    --
    Es gibt viel zu tun - packen wir's an: * ToDo-Liste * gewünschte Seiten
  3. CSS-Files werden manchmal so groß, dass man diese erst minifizieren und dann auch noch kuhzippen will. Auch sollen die Browser diese cachen, wozu es ggf. spezielle HTTP-Header braucht.

    Warum nur CSS und nicht auch JavaScript?

    IMHO ist die Minifizierung einzelner Dateien eher ein Placebo. Komprimierung hingegen ist natürlich hilfreich. Ob minifiziert oder nicht minifiziert komprimiert wird, macht keinen nennenswerten Unterschied. OK, der Client muss dann weniger parsen und Google Pagespeed findet das auch toll. Vernünftige HTTP-Header sind auch sinnvoll. Nur stellt sich dann die Frage, ob man PHP dafür braucht. Sowohl die Komprimierung, wie auch die Header kann der Webserver. Wenn nötig, via htaccess.

    Bleibt also die Minifizierung als einziger Mehrwert. Da bin ich skeptisch, ob man da für die Minifizierung einzelner Files PHP anwerfen sollte.

    Die Nummer lohnt sich m.E. erst, wenn man einzelne Ressourcen zusammenfasst und damit Requests einspart. "minify.php?/css/1.css,/css/2.css", bzw. "minify.php?/js/1.js,/js/2.js". Das alles gibt es schon ziemlich ausgereift:

    https://github.com/mrclay/minify

    1. Moin!

      Sowohl die Komprimierung, wie auch die Header kann der Webserver. Wenn nötig, via htaccess.

      Äh. Ja. Aber das führt dazu, dass der Webserver selbst komprimieren muss. Das bedeutet ein Absinken der Netzlast, welches durch einen Anstieg der Prozessorlast erkauft wird. Zudem berechnet der Apache den Etag merkwürdigerweise (per default) auch aus dem Inode der Datei. Das das Minifizieren nicht wirklich viel bringt weiß ich übrigens auch. Wollen aber trotzdem viele haben...

      Mein Konzept ist es, die Komprimierung und das Berechnen des Etag nur einmal durchzuführen.

      Warum nur CSS und nicht auch JavaScript?

      Nun, für Javascript (und also auch in HTML enthaltenes JS habe ich derzeit noch keine Methode gefunden um es in purem PHP ohne großen Aufwand(!) funktionssicher(!) zu minifizieren. Aber generell ist das Skript natürlich anpass- und verwendbar. So könnte man das Minifizieren weglassen...

      (Nicht gestellte Frage: Und die Webseiten selbst?)

      Eine ähnliche Methode für das "Einfaches Caching für Webprojekte" verwende ich seit vielen Jahren.

      Jörg Reinholz

      1. Nun, für Javascript (und also auch in HTML enthaltenes JS habe ich derzeit noch keine Methode gefunden um es in purem PHP ohne großen Aufwand(!) funktionssicher(!) zu minifizieren.

        https://github.com/pagespeed/mod_pagespeed

      2. (Nicht gestellte Frage: Und die Webseiten selbst?)

        https://www.varnish-cache.org/

        1. Moin!

          (Nicht gestellte Frage: Und die Webseiten selbst?)

          https://www.varnish-cache.org/

          1,9 MB tar.gz. Muss kompiliert werden - was nicht jeder kann und von denen, die es können, dann längst nicht jeder auf dem Webserver installieren darf. Ich schrieb aber: "in purem PHP ohne großen Aufwand".

          Einen Proxy wie Squid "vor den Webserver stellen" kann ich auch ...

          mod_pagespeed

          ... is an open-source Apache module (ähnlich wie oben) und also auch keine Lösung unter den oben genannten Prämissen.

          Jörg Reinholz

      3. Tach,

        Äh. Ja. Aber das führt dazu, dass der Webserver selbst komprimieren muss. Das bedeutet ein Absinken der Netzlast, welches durch einen Anstieg der Prozessorlast erkauft wird.

        wenn das ein relevanter Faktor ist, hat man ganz andere Probleme und kann vermutlich bereits mit Lastspitzen nicht mehr umgehen.

        mfg
        Woodfighter

        1. Moin!

          Absinken der Netzlast, welches durch einen Anstieg der Prozessorlast erkauft

          wenn das ein relevanter Faktor ist, hat man ganz andere Probleme und kann vermutlich bereits mit Lastspitzen nicht mehr umgehen.

          Das ist zu kurz gegriffen. Man kann nämlich auch Atomkraftwerke betreiben statt Energie zu sparen.

          Jörg Reinholz

          1. Tach,

            Das ist zu kurz gegriffen. Man kann nämlich auch Atomkraftwerke betreiben statt Energie zu sparen.

            dann musst du jetzt darlegen, dass dein PHP-Script beim Aufruf weniger Energie verbraucht, als den Webserver die Komprimierung übernehmen zu lassen und wenn das so wäre (was ich durchaus anzweifeln würde), wäre es vermutlich immer noch sinnvoller den Webserver (sofern er die komprimierten Dateien nicht eh schon cached), die einmalig komprimierten Dateien ausliefern zu lassen (siehe z.B. https://httpd.apache.org/docs/current/mod/mod_deflate.html#precompressed).

            mfg
            Woodfighter

            1. Moin!

              Atomkraftwerke vers. Energiesparen

              wäre es vermutlich immer noch sinnvoller den Webserver (sofern er die komprimierten Dateien nicht eh schon cached), die einmalig komprimierten Dateien ausliefern zu lassen (siehe z.B. https://httpd.apache.org/docs/current/mod/mod_deflate.html#precompressed).

              mfg
              Woodfighter

              Since mod_deflate re-compresses content each time a request is made, some performance benefit can be derived by pre-compressing the content and telling mod_deflate to serve them without re-compressing them. This may be accomplished using a configuration like the following:

              Übersetzt:

              Da mod_deflate jedes Mal wenn ein Request erfolgt die Inhalte erneut komprimiert können einige Leistungsvorteil durch Vorkomprimierung und die Konfiguration von mod_deflate erreicht werden - in dem vermieden wird, diese erneut zu komprimieren. Dies kann unter Verwendung einer Konfiguration wie folgt erreicht werden.

              Aus der Dokumentation:

                  RewriteCond "%{HTTP:Accept-encoding}" "gzip"
                  RewriteCond "%{REQUEST_FILENAME}\.gz" -s
                  RewriteRule "^(.*)\.css" "$1\.css\.gz" [QSA]
              

              Für Menschen, die "English4poors" nicht können, übersetzt:

              Wenn der Useragent meldet, dass er gzip kann, dann wird bei einem Request nach einer css-Datei geschaut, ob es deren gezippte Version schon gibt - und (wenn beides passt) - die gezippte Version ausgeliefert. Was hier nicht steht, ist, dass auch der Etag noch vom Apache berechnet werden muss.

              ~Genau das~ (Mist: Durchstreichen geht nicht.) Ähnliches/Etwas wichtiges mehr macht mein Skript!

              Wenn der Useragent meldet, dass er gzip kann, dann wird bei einem Request nach einer css-Datei geschaut, ob es deren gezippte Version schon gibt - und (wenn beides passt) - die gezippte Version ausgeliefert. Gibt es die gezippte Version nicht, dann wird diese erzeugt und ausgeliefert. Kann der useragent (angeblich) kein gzip, dann gibt es nur die minifizierte Version. Auch der Etag wird "vorgespeichert".

              Da ist dann doch ein kleiner Unterschied - oder?

              Für mein Skript reicht es, das CSS-File zu verändern und eine der gezippten Dateien oder das .etag - File zu löschen. Der Rest wird "einfach, schnell und leise" beim nächsten Abruf erledigt.

              Jetzt stell Dir mal einen Windows-Benutzer vor, der ohne Putty & Co. zu kennen oder einen ssh-Zugang zu haben (was mir als unbenutzbar gilt, aber viele Hoster noch immer so verkaufen), mod_gzip benutzen soll, also die Komprimate selbst erzeugen und ablegen muss.

              Jörg Reinholz

              1. Tach,

                ~Genau das~ (Mist: Durchstreichen geht nicht.) Ähnliches/Etwas wichtiges mehr macht mein Skript!

                Wenn der Useragent meldet, dass er gzip kann, dann wird bei einem Request nach einer css-Datei geschaut, ob es deren gezippte Version schon gibt - und (wenn beides passt) - die gezippte Version ausgeliefert. Gibt es die gezippte Version nicht, dann wird diese erzeugt und ausgeliefert. Kann der useragent (angeblich) kein gzip, dann gibt es nur die minifizierte Version. Auch der Etag wird "vorgespeichert".

                Da ist dann doch ein kleiner Unterschied - oder?

                ja, aber dein Script ist trotzdem eine Nischenlösung und die (vermutlich) besseren Lösungen sollten auch erwähnt werden, oder? Mindestens sollten vermutlich falsche Aussage, wie dein Script wäre energiesparender entkräftet werden.

                mfg
                Woodfighter

                1. Moin!

                  Mindestens sollten vermutlich falsche Aussage, wie dein Script wäre energiesparender entkräftet werden.

                  Nun ja. Mit "vermutlich falsche Aussage" lässt Du ja selbst offen, dass es Dir nicht ganz klar ist ob diese Methode gegenüber allen vorstellbaren "energetisch teurer" ist. Freilich ist die von Dir vorgestellte, "native" Methode mit dem manuellen Erzeugen der Komprimate die energiesparenste. Aber wer verzichtet denn auf den Staubsauger, räumt die Wohnung aus und klopft wie "anno früher" den Teppich?

                  Jörg Reinholz

                  1. Tach,

                    Nun ja. Mit "vermutlich falsche Aussage" lässt Du ja selbst offen, dass es Dir nicht ganz klar ist ob diese Methode gegenüber allen vorstellbaren "energetisch teurer" ist.

                    ja, aber die Aussage war deine, und damit ach die Nachweispflicht.

                    Freilich ist die von Dir vorgestellte, "native" Methode mit dem manuellen Erzeugen der Komprimate die energiesparenste. Aber wer verzichtet denn auf den Staubsauger, räumt die Wohnung aus und klopft wie "anno früher" den Teppich?

                    Das Energie-Argument war deins nicht meins, und ich würde nicht auf die Idee kommen, das komprimieren nicht zu automatisieren.

                    mfg
                    Woodfighter

                    1. Moin!

                      ja, aber die Aussage war deine, und damit ach die Nachweispflicht.

                      Nein. Ich hatte nicht ausgesagt, dass die Methode energiesparend ist. Das ich derlei damit gemeint haben könnte ist Deine Mutmaßung. Es handelte sich um eine Antwort auf Deine Bemerkung "wenn das ein relevanter Faktor ist, hat man ganz andere Probleme und kann vermutlich bereits mit Lastspitzen nicht mehr umgehen." die auf meinen Einwurf "Aber das führt dazu, dass der Webserver selbst komprimieren muss. Das bedeutet ein Absinken der Netzlast, welches durch einen Anstieg der Prozessorlast erkauft wird." hin erfolgte.

                      Verstehst Du, dass es gar nicht um Energie an sich, sondern um einen Vergleich mit der Aussage ging, dass man mit Ressourcen (hier eher bezogen auf Netz- und Prozessorlast) doch bitte stets sparsam umgehen solle?

                      Jörg Reinholz

                      1. Tach,

                        Verstehst Du, dass es gar nicht um Energie an sich, sondern um einen Vergleich mit der Aussage ging, dass man mit Ressourcen (hier eher bezogen auf Netz- und Prozessorlast) doch bitte stets sparsam umgehen solle?

                        ah sorry, dann habe ich dein Bild zu wörtlich genommen; allerdings glaube ich weiterhin nicht, dass dein Script ressourcenschonender ist als die Alternativen. Und in diesem Falle handelt es sich um eine Ressource, die auf Webservern fast immer im Überfluss vorhanden ist; ich habe im wesentlichen nur reagiert, weil die selbe falsche Argumentation auch gerne in Bezug auf TLS verwendet wird.

                        mfg
                        Woodfighter

              2. Hallo Jörg Reinholz,

                ~Genau das~ (Mist: Durchstreichen geht nicht.) Ähnliches/Etwas wichtiges mehr macht mein Skript!

                Durchstreichen geht.

                Bis demnächst
                Matthias

                --
                Das Geheimnis des Könnens liegt im Wollen. (Giuseppe Mazzini)
                1. Moin!

                  Durchstreichen geht.

                  Aha.

                  Jörg Reinholz