...

PHP Preloading: Performance-Booster für moderne PHP 8 Projekte

PHP Preloading in PHP 8 lädt zentrale Klassen und Funktionen beim Start von PHP-FPM in den Speicher und verkürzt so den Weg bis zur eigentlichen Anwendungslogik deutlich. Ich zeige, wie ich Preloading mit OPcache kombiniere, wo es messbar Tempo bringt und wie ich es sicher in Builds und Deployments verankere.

Zentrale Punkte

Bevor ich tiefer einsteige, fasse ich die wichtigsten Aspekte zusammen und ordne sie für die Praxis ein. Ich erkläre kurz die Beziehung zwischen OPcache und Preloading und lege dar, warum der Effekt gerade bei großen Frameworks greift. Danach gehe ich auf harte Zahlen zu Latenz, Durchsatz und Autoloading ein, damit die Erwartungen realistisch bleiben. Ich beleuchte außerdem, wann ich Preloading einsetze und wann ich es weglasse, um Aufwand zu sparen. Am Ende gebe ich Hinweise für Konfiguration, Skripte, Tests und einen sauberen Neustart im Betrieb.

  • Mechanik: vorkompilierte Klassen/Funktionen bleiben prozessweit verfügbar.
  • Leistung: minus 5–15 % TTFB, plus 30–50 % RPS möglich.
  • Autoloading: 10–16 ms pro Request einsparen.
  • Auswahl: nur stabile Kernmodule und Framework-Basis einbinden.
  • Deployment: Änderungen erfordern FPM-Neustart mit Plan.

OPcache vs. Preloading: kurzer Überblick

OPcache kompiliert Dateien beim ersten Aufruf in Bytecode und legt diesen im Speicher ab, während Preloading gezielt und einmalig beim Start abläuft. Ich nutze Preloading, um Kernklassen vorab zu kompilieren und dauerhaft in einem persistenten Speicherteil von OPcache zu halten. Dadurch liegen essenzielle Symbole ohne Autoloading, Datei-Parsing oder include/require direkt bereit. Der normale OPcache kann Einträge bei Speicher- oder Zeitdruck verwerfen, die vorab geladenen Elemente bleiben jedoch erhalten. So spare ich I/O, behebe Kaltstart-Bremsen und reduziere CPU-Zeit in der frühen Bootstrap-Phase großer Apps.

Wie Preloading den Request-Zyklus verkürzt

Eine typische Anfrage lädt erst Hunderte Dateien, bevor Controller und Business-Code laufen, und genau hier greift Preloading. Ich lege Kernbestandteile von Frameworks wie Symfony oder Laravel vorab in den Cache und entferne damit wiederholtes Parsen vieler Dateien. Das senkt die Time To First Byte oft um 5–15 % und verschafft mehr Headroom für echte Logik. Autoloading-Ketten entfallen für Core-Klassen, was gerade bei Antwortzeiten unter 200 ms spürbar wird. Unter Last steigen Requests pro Sekunde, weil mehr CPU-Zeit der eigentlichen Anwendung zugutekommt.

Wann Preloading wirklich trägt

Ich aktiviere Preloading vor allem bei großen Framework-Stacks, APIs und Shop-Systemen mit vielen Klassen, da dort der Kaltstart kostet. In solchen Umgebungen bringen 30–50 % mehr RPS realen Nutzen, besonders wenn Hardware unverändert bleiben soll. Kleine Skripte oder simple Seiten mit wenigen Includes profitieren kaum, weil der Overhead gering ist. WordPress gewinnt dann, wenn viele Plugins und eigene Libraries im Hintergrund werkeln. In allen Fällen zählt eine saubere Auswahl der zu ladenden Dateien, sonst frisst der Cache unnötig Speicher.

Grenzen und Fallstricke im Alltag

Preloading bleibt statisch, bis ich den FPM-Pool neu starte, und genau das erfordert Disziplin im Deployment. Sobald ich geänderte preloaded Dateien aufspiele, sehen laufende Prozesse noch den alten Bytecode. Deshalb plane ich Neustarts kontrolliert und lade keine oft wechselnden Artefakte wie generierte Klassen vor. Ich achte außerdem auf OPcache-Speicher und maximale beschleunigte Dateien, damit nichts aus dem Cache fällt. Wer sich tiefer mit inkonsistenten Caches und Neustarts befasst, findet Hintergründe zur OPcache-Invalidierung, die ich in großen Setups immer mitdenke.

OPcache- und Preload-Konfiguration in PHP 8

Für einen guten Start aktiviere ich OPcache, dimensioniere den Speicher und definiere das Preload-Skript samt Benutzer, damit keine Rechte-Probleme auftreten. Wichtige Schalter sind zend_extension, opcache.enable, memory_consumption, max_accelerated_files und die Pfade zu opcache.preload sowie opcache.preload_user. Ich setze dabei auf konsistente Einstellungen je FPM-Pool, weil Mischtöpfe schnell zu Fehlersuche führen. Die folgenden Parameter nutze ich als Anhaltspunkt und passe sie an Projektgröße und Traffic an. Wer die Optionen vertiefen möchte, findet praktische Hinweise zur OPcache-Konfiguration, die ich bei jedem feinen Tuning prüfe.

Einstellung Beispielwert Wirkung
opcache.enable 1 Aktiviert OPcache global.
opcache.memory_consumption 256–512 Reserviert MB für Bytecode und Symbole.
opcache.max_accelerated_files 20000–100000 Erhöht die Zahl gecachter Dateien.
opcache.preload /pfad/zu/preload.php Definiert das Preload-Skript.
opcache.preload_user www-data Legt den Ausführungsnutzer fest.

Ein Preload-Skript aufsetzen

In preload.php liste ich Kernklassen explizit oder kompiliere ausgewählte Verzeichnisse rekursiv mit opcache_compile_file(). Ich beginne mit Framework-Basis und stabilen Modulen aus src/, damit ich maximale Trefferquote im heißen Pfad erreiche. Vendor komplett zu laden, bläht den Cache meist auf und erhöht Risiko beim Deployment. Besser ist eine kurze White-List für Framework-Kern und eine wohldosierte automatische Einbindung eigener Module. Mit Kommentaren und einer Versionskennung im Skript halte ich den Überblick und steuere Neustarts bewusst, statt Zufall das Feld zu überlassen.

Messen, validieren, nachjustieren

Ich aktiviere Preloading nie blind, sondern messe zuerst Baselines für TTFB, CPU-Last, Speicher und RPS. Danach drehe ich an der Auswahl der Dateien und prüfe erneut, ob Autoloading und Datei-Zugriffe sinken. Einfache Request-Logs zeigen schnell, wie viele Includes wegfallen und wo noch Flaschenhälse lauern. Für die Wirkung unter Last setze ich wiederholbare Benchmarks an, etwa mit identischen Szenarien pro Build. Wenn die Zahlen passen, friere ich die Preload-Liste ein und dokumentiere den Prozess in CI/CD.

Preloading in DevOps- und Deployment-Prozesse einbinden

Ich binde das Preload-Skript in den Build ein, lasse Artefakte prüfen und stoße am Ende einen geplanten FPM-Neustart an. Rollbacks berücksichtigen immer die gepinnte Preload-Version, damit alte Prozesse konsistent bleiben. Blue/Green oder Canary verringern das Risiko, während ich die neue Konfiguration ausrolle. Für Wartungsfenster priorisiere ich kurzlebige Pools und verschiebe schreibintensive Vorgänge, bis die Knoten wieder warm sind. So halte ich Latenzspitzen klein und verhindere Bytecode-Mischzustände auf Servern.

Hosting-Strategie: Wann Server-Setup den Unterschied macht

Ein performanter Stack mit PHP 8.x, schneller NVMe, genügend RAM und passenden OPcache-Limits lässt Preloading glänzen. Ich achte darauf, dass FPM-Pools gleichartig konfiguriert sind und genug Puffer für den persistenten Bytecode bleibt. Je nach Projektphase passe ich Prozessanzahl, Memory und Max-Files an, um Garbage im Cache zu vermeiden. Für Versionswechsel prüfe ich Nebenwirkungen, weil Änderungen an Engine-Interna Einfluss auf Bytecode haben können. Wer Setup und Versionen sinnvoll koppelt, profitiert messbar; Hinweise zu PHP-Version und Hosting nutze ich als Leitplanke beim Sizing.

Praxis-Checkliste für Projekte

Ich starte mit einem Preload-Pilot auf Staging und sichere mir belastbare Vorher/Nachher-Werte. Danach wähle ich die 50–200 heißesten Klassen aus Framework und Kernmodulen, statt den ganzen Vendor zu laden. Ich dokumentiere Neustarts, verknüpfe Preload-Versionen mit Builds und rolle Updates gruppenweise aus. Für Wartung halte ich Skript, OPcache-Parameter und Messpunkte im Repository, damit jeder Change nachvollziehbar bleibt. Mit diesem Vorgehen erziele ich kürzere TTFB, mehr RPS und ruhigere Lastkurven ohne Überraschungen.

Feineinstellungen, die oft vergessen werden

Neben den Kernparametern lohnt der Blick auf ein paar Stellschrauben, die das Ergebnis stabilisieren:

  • opcache.interned_strings_buffer: 16–64 MB einplanen. Große Frameworks profitieren, weil viele identische Strings (Namespaces, Methodennamen) nur einmal im Speicher liegen.
  • opcache.save_comments: Auf 1 belassen, wenn Attribute/Annotations genutzt werden. Wer Kommentare wegspart, riskiert unerwartetes Verhalten bei Reflection und Validatoren.
  • opcache.validate_timestamps: In Produktion oft 0, damit OPcache nicht ständig das Dateisystem prüft. In Kombination mit Preloading ist das stimmig, weil Änderungen ohnehin einen Neustart erfordern.
  • opcache.revalidate_freq: Falls validate_timestamps=1 (z. B. Staging), Frequenz hoch setzen (z. B. 60), um Dateisystem-Last zu senken.
  • opcache.jit und jit_buffer_size: JIT bringt Web-Workloads selten den großen Sprung, belegt aber Speicher. Ich halte JIT konservativ oder aus, solange nicht messbar nötig, um Preload-Speicher nicht zu kannibalisieren.

Geeignete Kandidaten auswählen

Die Auswahl entscheidet über Wirkung und Stabilität. Ich gehe dabei datengetrieben vor:

  • Include-Statistiken: Im Access-Log oder Profiler (Xdebug/Blackfire) sehe ich, welche Dateien pro Request am häufigsten geladen werden.
  • Composer-Classmap: Mit optimiertem Autoloader (dump-autoload -o) habe ich eine gute Grundlage, um stabile Namespaces aus core und src zu identifizieren.
  • Framework-Kern: In Symfony etwa HttpKernel, EventDispatcher, Routing, DI-Container-Basis; in Laravel Foundation, Support und Teile von Illuminate.
  • Eigene Basismodule: Wertobjekte, Utility-Layer, zentrale Interfaces und Traits, die nahezu jeder Request nutzt.

Nicht preloaden: dynamisch generierte Artefakte (Proxies, Compiled Container, Caches), stark wechselnde Domain-Klassen während aktiver Entwicklung oder selten genutzte Admin-Module.

Beispiel: robustes Preload-Skript

Ein kurzer, wartbarer Ansatz, der nur gewünschte Bereiche kompiliert und sauber protokolliert:

<?php
// preload.php v1.3 - Auswahl nur stabiler Kernmodule

$root = __DIR__;
$paths = [
    $root . '/src/Domain',
    $root . '/src/Application',
    $root . '/vendor/symfony/http-kernel',
    $root . '/vendor/symfony/event-dispatcher',
    $root . '/vendor/illuminate/support',
];

// Hilfsfunktion: rekursiv PHP-Dateien kompilieren
function preload_dir(string $dir): void {
    $it = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
    );
    foreach ($it as $file) {
        if ($file->isFile() && $file->getExtension() === 'php') {
            @opcache_compile_file($file->getPathname());
        }
    }
}

foreach ($paths as $path) {
    if (is_dir($path)) {
        preload_dir($path);
    }
}

// Einzeldateien, die nicht in obigen Ordnern liegen
$single = [
    $root . '/src/Kernel.php',
    $root . '/src/Infrastructure/Bootstrap.php',
];

foreach ($single as $file) {
    if (is_file($file)) {
        @opcache_compile_file($file);
    }
}

// Optional: Marker in die Error-Log ausgeben
error_log('[preload] completed at ' . date(DATE_ATOM));

Wichtig: Ich arbeite mit absoluten Pfaden, vermeide require/inkludierte Seiteneffekte im Preload-Skript und halte die Liste stabil. opcache_compile_file() kompiliert, ohne die Datei auszuführen – so verhindere ich, dass Bootstrap-Code zur Preload-Zeit läuft.

Framework-Besonderheiten

In Symfony verzahne ich Preloading mit dem Cache-Warmup: Erst baue ich Container und Routen-Cache, dann kompiliere ich stabile Kernklassen. Proxies und der generierte Container selbst bleiben draußen, da sich ihre Dateinamen und Inhalte pro Build ändern können. In Laravel gilt ähnliches für Config/Route/View-Caches: Sie helfen dem Start, sind aber wegen häufiger Änderung keine guten Preload-Kandidaten. WordPress profitiert, wenn ich die Hot-Paths großer Plugins (CPT-Registrierung, Shortcode-Parser, Query-Utils) wähle, ohne das komplette vendor-Verzeichnis zu ziehen.

Sicherheit und Rechte

Weil Preloading beim FPM-Start unter opcache.preload_user läuft, stelle ich sicher, dass dieser Nutzer Lesezugriff auf alle vorab zu kompilierenden Dateien hat. Ich preloadiere nur signierten, geprüften Code aus dem Build-Artefakt. Experimentelle oder ungetestete Pakete haben im Preload nichts verloren, da ein Fehler den gesamten Pool aus dem Tritt bringen kann. In Multi-Tenant-Szenarien trenne ich Preload-Skripte je Pool, um Leaks zwischen Projekten zu vermeiden.

Diagnose und Monitoring

Für den Betrieb brauche ich schnelle Checks:

  • phpinfo(): Zeigt, ob Preloading aktiv ist und welche Datei als opcache.preload gesetzt ist.
  • opcache_get_status(): Gibt Speicherbelegung, gecachte Skripte und Wasted-Memory aus; ich prüfe insbesondere die verbleibenden freien MB und die Anzahl beschleunigter Dateien.
  • Logs: Das Preload-Skript kann eine kurze Erfolgsmeldung ins Error-Log schreiben; bei Fehlern sehe ich dort Pfad- oder Rechteprobleme.
  • Metriken: Ich beobachte TTFB, CPU-Load und 95./99.-Perzentile der Antwortzeiten vor/nach Neustarts, um Regressionen früh zu entdecken.

Typische Stolpersteine

  • Reload vs. Restart: Ein FPM-reload reicht nicht, um Preload-Änderungen zu greifen. Ich plane einen echten Neustart des Pools ein.
  • Speicherknappheit: Wenn opcache.memory_consumption zu klein ist, verdrängt OPcache normale Skripte oder verweigert neue Einträge. Ich reserviere großzügig und prüfe nach dem Warmup, wie viel Puffer bleibt.
  • Zu breite Auswahl: Ein voller vendor-Preload erhöht Speicher, steigert aber selten den Hit-Ratio. Ich bleibe selektiv und messe.
  • Seiteneffekte beim Preload: Niemals Dateien mit globalem Code includen, der DB-Verbindungen aufbaut oder Umgebungsvariablen voraussetzt. Ich nutze opcache_compile_file() statt require.
  • Inkonsistente Pfade: Relative Pfade können in Container- oder Chroot-Umgebungen brechen. Ich arbeite streng mit absoluten Pfaden.

Container- und Orchestrierungs-Setup

In Containern startet Preloading bei jedem neuen Pod/Container neu. Das ist gut für Konsistenz, kann aber die erste Minute verlangsamen. Ich löse das so:

  • Readiness-Probe: Der Pod signalisiert erst dann „ready“, wenn das Preload-Skript durchgelaufen ist und OPcache stabil befüllt ist.
  • Warmup-Request: Nach dem Start feuere ich gezielte Requests auf Hot-Endpoints, um auch nicht-preloadete, aber häufige Pfade zu initialisieren.
  • Gedrosseltes Rolling Update: Kleine Batches für neue Pods, damit nicht alle Instanzen gleichzeitig im Kaltstart sind.

Rollback und Notfallplan

Wenn eine Preload-Änderung Probleme macht, will ich schnell zurückdrehen können:

  • Versioniertes Preload-Skript: Jede Build-Nummer referenziert eine definierte Preload-Version.
  • Schneller Toggle: Ich halte eine Konfigurationsvariante bereit, die opcache.preload kurzfristig deaktiviert, bis die Ursache geklärt ist.
  • Gezielter Neustart: Zuerst kleine Pools oder ein Canary-Knoten, dann gestaffelt die restlichen Instanzen.

Was Preloading nicht löst

Preloading beschleunigt den PHP-Bootstrap, aber es ersetzt keine Datenbank-Optimierung, kein Caching von HTTP-Responses und keine asynchronen Prozesse. Wenn externe Dienste oder Queries die meiste Zeit kosten, zeigt Preloading nur begrenzten Effekt. In solchen Fällen priorisiere ich Query-Tuning, Response-Caches und Queue-basierte Workflows – Preloading wird dann zur Abrundung des Gesamtsystems.

Realistische Erwartungen pro Projektphase

  • Greenfield/Frühe Entwicklung: Ich verzichte oft auf Preloading in lokalen Setups, um Änderungen ohne Neustarts zu sehen. Auf Staging teste ich selektiv.
  • Feature-Freeze: Jetzt zahlt Preloading aus – stabile Kernmodule bündeln und Zielwerte für TTFB und RPS mit Lasttests absichern.
  • Langfristiger Betrieb: Einmal im Quartal prüfe ich, ob die Preload-Liste noch zu den Hot-Paths passt. Neue Module wandern erst nach Messung hinein.

Kurzbilanzen für schnelle PHP‑8‑Projekte

Preloading ergänzt OPcache ideal, weil es zentrale Klassen und Funktionen beim Prozessstart dauerhaft bereitstellt. In großen Projekten senke ich damit Autoloading-Kosten, Dateizugriffe und Parsing-Aufwand, wodurch TTFB oft um 5–15 % fällt. Bei API- und Shop-Workloads steigt der Durchsatz teils um 30–50 %, solange die Datenbank und externe Dienste mithalten. Den größten Gewinn erziele ich mit einer klaren Auswahl, sauberen OPcache-Parametern, Tests unter Last und geplanten Neustarts. Wer diese Punkte beherzigt, holt aus PHP 8 konstant mehr Tempo heraus und hält Antwortzeiten selbst bei Spitzen verlässlich niedrig.

Aktuelle Artikel

WordPress Server mit Redis Full-Page-Cache für schnelle Ladezeiten
Wordpress

Redis als Full-Page-Cache in WordPress: Grenzen und Möglichkeiten

Redis Full-Page-Cache beschleunigt WordPress, indem komplette Seiten im Arbeitsspeicher liegen. Erfahre, wie der Cache funktioniert, welche Grenzen es gibt und wie du den Fokus Keyword redis full page cache optimal im Setup nutzt.