Pliki statyczne

OxPHP serwuje pliki statyczne prosto z katalogu głównego dokumentów, bez uruchamiania PHP. Każdy plik jest serwowany z automatycznym wykrywaniem typu MIME, buforem w pamięci zapewniającym szybki dostęp przy powtarzających się żądaniach oraz pełnym buforowaniem HTTP: nagłówkami ETag, żądaniami warunkowymi i żądaniami Range dla pobrań częściowych.

Jak to działa

Gdy żądanie pasuje do pliku statycznego:

  1. Dopasowanie pliku — warstwa routingu rozwiązuje ścieżkę URL do pliku na dysku
  2. Wykrywanie typu MIME — typ zawartości jest ustalany na podstawie rozszerzenia pliku
  3. Sprawdzenie bufora — bufor plików jest sprawdzany przed sięgnięciem do systemu plików
  4. Sprawdzenie warunkowe — jeśli żądanie zawiera If-None-Match lub If-Modified-Since, OxPHP ocenia warunek i może zwrócić 304 Not Modified bez wysyłania ciała odpowiedzi
  5. Sprawdzenie zakresu — jeśli żądanie GET lub HEAD zawiera nagłówek Range, OxPHP odpowiada kodem 206 Partial Content: GET otrzymuje tylko żądany zakres bajtów, a HEAD te same nagłówki zakresu bez ciała
  6. Odpowiedź — pliki do 1 MiB są serwowane z bufora w pamięci; większe pliki są strumieniowane bezpośrednio z dysku, chyba że mają zostać skompresowane

Konfiguracja

Zmienna Wartość domyślna Opis
STATIC_MAX_AGE 30d Cache-Control: max-age dla plików statycznych. Przyjmuje 30s, 5m, 2h, 30d, 1w, 1y, samą liczbę sekund (np. 3600) lub off, aby całkowicie wyłączyć nagłówki buforowania. Zastępuje przestarzałą zmienną STATIC_CACHE_TTL.
STATIC_REVALIDATE off Ustaw na on, aby włączyć rewalidację na podstawie mtime dla bufora zawartości w pamięci (ponowne sprawdzenie każdego pliku najwyżej raz na 3 sekundy; zmiany stają się widoczne w tym oknie czasowym). Zastępuje przestarzałą zmienną STATIC_CACHE (w której off miało odwrotne znaczenie).

Wykrywanie typu MIME

Typy MIME są ustalane automatycznie na podstawie rozszerzenia pliku. Jeśli nie da się ustalić typu, serwer używa domyślnie application/octet-stream. Do typowych odwzorowań należą:

Rozszerzenie Content-Type
.html text/html
.css text/css
.js text/javascript
.json application/json
.png image/png
.svg image/svg+xml
.woff2 font/woff2

Buforowanie plików

OxPHP używa bufora w pamięci, aby ograniczyć operacje I/O na dysku dla często żądanych plików:

  • Pliki do 1 MiB (1 048 576 bajtów) są wczytywane do pamięci i buforowane. Całkowity budżet bufora wynosi 64 MiB (67 108 864 bajtów). Po przekroczeniu budżetu najdawniej używane wpisy są usuwane, aby zwolnić miejsce.
  • Pliki większe niż 1 MiB są strumieniowane bezpośrednio z dysku zamiast trafiać do bufora. Nagłówek Content-Length jest ustawiany na podstawie metadanych pliku, dzięki czemu klient zna z góry całkowity rozmiar.
  • Jedynym wyjątkiem od strumieniowania jest kompresowalny plik, który wciąż mieści się w oknie kompresji (3 MiB). Dla klientów akceptujących kodowanie zawartości jest on wczytywany w całości, ponieważ serwer nie może zakodować ciała, którego nie ma w całości w pamięci. Plik przekracza limit bufora, więc jest kompresowany od nowa przy każdym takim żądaniu. Umieść przed zasobami z tego przedziału sieć CDN albo utrzymuj je poniżej 1 MiB, a praca zostanie wykonana tylko raz. Jednocześnie przechowywana jest ograniczona liczba takich plików; żądanie, które przyjdzie ponad ten limit, zamiast czekać na wolne miejsce, otrzymuje plik strumieniowany i niezakodowany.

Bufor plików jest wypełniany przy pierwszym żądaniu każdego pliku i utrzymywany między kolejnymi żądaniami. Domyślnie wpisy bufora pozostają, dopóki nie zostaną usunięte przez politykę LRU.

Rewalidacja zawartości

Ustaw STATIC_REVALIDATE=on, aby włączyć rewalidację na podstawie mtime. W tym trybie serwer ponownie sprawdza czas modyfikacji buforowanego pliku wywołaniem systemowym stat() najwyżej raz na 3 sekundy dla każdego pliku, a nie przy każdym żądaniu. Jeśli plik zmienił się na dysku, nieaktualny wpis jest usuwany, a plik zostaje automatycznie wczytany ponownie. W oknie 3-sekundowym buforowany wpis jest serwowany prosto z pamięci bez wywołania systemowego, więc koszt jest amortyzowany, a nie płacony przy każdym żądaniu. Zmiany na dysku stają się widoczne w ciągu 3 sekund.

Praca deweloperska

Ustaw STATIC_REVALIDATE=on podczas pracy deweloperskiej, aby widzieć zmiany w plikach bez restartowania serwera. W środowisku produkcyjnym pozostaw tę zmienną nieustawioną (domyślne off), aby uzyskać maksymalną przepustowość przy zerowym narzucie wywołań systemowych na żądanie.

Buforowanie HTTP

Cache-Control

Gdy ustawiona jest zmienna STATIC_MAX_AGE (domyślnie 30d), każda odpowiedź z plikiem statycznym zawiera nagłówek Cache-Control:

http
Cache-Control: public, max-age=2592000

Wartość max-age to TTL przeliczony na sekundy. Ustaw STATIC_MAX_AGE=off, aby całkowicie pominąć ten nagłówek.

ETag i Last-Modified

Każda odpowiedź z plikiem statycznym zawiera:

  • ETag — silny ETag w formacie "<size>-<mtime_hex>", wyprowadzony z rozmiaru pliku i czasu ostatniej modyfikacji. Silny walidator spełnia też warunek If-Range, dzięki czemu przerwane pobrania można bezpiecznie wznowić. Gdy odpowiedź jest serwowana skompresowana, niezależnie od kodowania, znacznik jest osłabiany do postaci W/"…": skompresowane bajty to inna reprezentacja, a słaby znacznik nadal umożliwia rewalidację (304), ale zapobiega mieszaniu skompresowanych i nieskompresowanych fragmentów przy wznawianiu.
  • Last-Modified — data HTTP zgodna z RFC 9110, oparta na czasie modyfikacji pliku

Nagłówki te pozwalają przeglądarkom i sieciom CDN walidować buforowane kopie bez ponownego pobierania pliku.

Żądania warunkowe (304)

OxPHP ocenia nagłówki żądań warunkowych, aby uniknąć wysyłania niezmienionej zawartości pliku:

  • If-None-Match — klient wysyła ETag, który ma w buforze. Jeśli pasuje on do bieżącego pliku, OxPHP zwraca 304 Not Modified bez ciała odpowiedzi.
  • If-Modified-Since — klient wysyła znacznik czasu. Jeśli plik nie był modyfikowany od tego momentu, OxPHP zwraca 304.

If-None-Match ma pierwszeństwo przed If-Modified-Since zgodnie z RFC 9110. Dla plików znajdujących się już w buforze w pamięci sprawdzenie warunkowe przebiega bez żadnych operacji I/O na dysku.

Żądania Range (206)

Odpowiedzi z plikami statycznymi ogłaszają Accept-Ranges: bytes, a żądania GET z pojedynczym zakresem w nagłówku Range otrzymują tylko żądane bajty:

http
GET /videos/intro.mp4 HTTP/1.1 Range: bytes=1048576- HTTP/1.1 206 Partial Content Content-Range: bytes 1048576-52428799/52428800 Content-Length: 51380224

Umożliwia to przewijanie elementów <video>/<audio> w przeglądarkach, wznawialne pobrania (wget -c, menedżery pobierania) oraz częściowe wczytywanie plików PDF. Obsługiwane są wszystkie trzy formy zakresu z RFC 9110: bytes=N-M, bytes=N- (od przesunięcia do końca) oraz bytes=-N (ostatnie N bajtów).

  • Zakres, którego nie da się spełnić (początek poza końcem pliku), zwraca 416 Range Not Satisfiable z nagłówkiem Content-Range: bytes */<size>.
  • If-Range jest respektowany: gdy klient wysyła ETag (lub datę Last-Modified) swojej częściowej kopii, a plik zmienił się od tamtej pory, OxPHP zwraca pełną odpowiedź 200 zamiast niepasującego fragmentu. Forma z datą jest akceptowana dopiero po pełnym upływie sekundy modyfikacji pliku — świeżo zapisany plik mógłby zmienić się ponownie w tej samej sekundzie bez zmiany daty, więc data nie jest jeszcze silnym walidatorem (RFC 9110).
  • Żądania z wieloma zakresami (bytes=0-1,4-5) otrzymują cały plik jako 200 OK — odpowiedzi multipart/byteranges nie są generowane.
  • Żądania HEAD z nagłówkiem Range otrzymują te same nagłówki 206/Content-Range co GET, ale bez ciała, zgodnie z zachowaniem nginx i Apache.
  • Zakresy i kompresja wzajemnie się wykluczają. Dla klientów akceptujących dowolne kodowanie zawartości obsługa zakresów jest wyłączana dla reprezentacji, które byłyby serwowane skompresowane, a skompresowane odpowiedzi nie ogłaszają Accept-Ranges, ponieważ w przeciwnym razie wznowione pobranie mogłoby doszyć nieskompresowane bajty do skompresowanego prefiksu. Pliki większe niż okno kompresji (3 MiB) nigdy nie są kompresowane, więc zakresy zawsze działają dla zawartości, która faktycznie ich potrzebuje: wideo, archiwów i obrazów. Odpowiedzi dla plików kwalifikujących się do kompresji zawsze niosą nagłówek Vary: Accept-Encoding, nawet gdy są serwowane nieskompresowane, dzięki czemu współdzielone bufory rozróżniają warianty.
  • Odpowiedzi 206 nigdy nie są kompresowane, a obsługa zakresów nie dotyczy odpowiedzi PHP — tylko plików statycznych.

Przykład: wznowienie przerwanego pobrania za pomocą curl:

bash
curl -C - -O https://example.com/dist/app-installer.dmg

Wyłączanie buforowania

Istnieją dwie niezależne warstwy bufora i jedna zmienna dla każdej z nich:

Zmienna Kontroluje Efekt off
STATIC_MAX_AGE=off Bufor przeglądarki (nagłówki HTTP) Nie są wysyłane nagłówki Cache-Control, ETag ani Last-Modified
STATIC_REVALIDATE=on Bufor serwera w pamięci Ponowne sprawdzanie mtime pliku najwyżej raz na 3 s dla każdego pliku; nieaktualne wpisy usuwane automatycznie

W środowisku deweloperskim ustaw STATIC_REVALIDATE=on, aby serwer zawsze serwował świeżą zawartość. Opcjonalnie ustaw też STATIC_MAX_AGE=off, aby całkowicie wyłączyć buforowanie w przeglądarce.

Rozwiązywanie problemów

Serwer wciąż serwuje nieaktualne pliki

Domyślnie bufor zawartości w pamięci nie sprawdza, czy pliki zmieniły się na dysku. Ustaw STATIC_REVALIDATE=on podczas pracy deweloperskiej, aby włączyć rewalidację na podstawie mtime — serwer automatycznie wykrywa zmiany plików (w ciągu 3 sekund).

Przeglądarka wciąż serwuje nieaktualne pliki

Jeśli serwer zwraca świeżą zawartość, a przeglądarka nadal pokazuje starą wersję, winowajcą jest własny bufor przeglądarki. Ustaw STATIC_MAX_AGE=off, aby przestać wysyłać nagłówki buforowania, albo użyj twardego przeładowania w przeglądarce (Shift+F5 lub Cmd+Shift+R).

Pliki są serwowane z typem `application/octet-stream`

OxPHP ustala typ MIME na podstawie rozszerzenia pliku. Jeśli rozszerzenia brakuje lub nie jest rozpoznane, używany jest domyślnie application/octet-stream. Dodaj poprawne rozszerzenie do pliku albo zadbaj o to, aby framework jawnie ustawiał nagłówek Content-Type w odpowiedziach PHP.

Duże pliki wydają się wolne

Pliki większe niż 1 MiB nie są buforowane w pamięci. Są strumieniowane z dysku przy każdym żądaniu albo, jeśli są kompresowalne i mniejsze niż 3 MiB, wczytywane i kompresowane od nowa przy każdym żądaniu od klienta akceptującego kompresję. W przypadku bardzo dużych plików umieść przed OxPHP sieć CDN, aby buforowała je na brzegu. Alternatywnie zrestrukturyzuj zasoby tak, aby często serwowane pliki mieściły się poniżej 1 MiB.

Zwracane są odpowiedzi 304, gdy oczekujesz 200

Kod 304 oznacza, że klient ma już bieżącą wersję. To poprawne zachowanie. Jeśli podczas pracy deweloperskiej musisz wymusić świeżą odpowiedź, ustaw STATIC_MAX_AGE=off, aby przestać wysyłać nagłówki ETag i Last-Modified.

Przykład dla Dockera

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "8080:80" volumes: - ./src:/var/www/html environment: - DOCUMENT_ROOT=/var/www/html/public - ENTRY_FILE=index.php - STATIC_MAX_AGE=1y

Dobre praktyki

  • Używaj długich TTL wraz z nazwami plików odpornymi na buforowanie (cache-busting) w środowisku produkcyjnym (np. app.a1b2c3.js). Ustaw STATIC_MAX_AGE=1y, aby uzyskać maksymalne buforowanie w przeglądarce i sieci CDN.
  • Ustaw STATIC_REVALIDATE=on podczas pracy deweloperskiej, aby serwer automatycznie wykrywał zmiany plików. Opcjonalnie ustaw też STATIC_MAX_AGE=off, aby pominąć buforowanie w przeglądarce.
  • Umieść sieć CDN przed OxPHP w przypadku witryn o dużym ruchu. Nagłówki ETag, Last-Modified i Cache-Control współpracują ze wszystkimi głównymi dostawcami CDN.
  • Pozwól narzędziu budującemu zająć się hashowaniem zasobów. Frameworki takie jak Vite i Laravel Mix automatycznie generują nazwy plików z hashami, dzięki czemu długie TTL bufora są bezpieczne.

Prompty dla Twojego agenta

Te prompty zakładają agenta z podłączonym serwerem MCP dokumentacji OxPHP — sam przeczyta strony, których potrzebuje.

prompt
Ustaw buforowanie plików statycznych OxPHP pod mój potok zasobów. Przeczytaj stronę `features/static-files` przez serwer MCP dokumentacji OxPHP. Sprawdź, czy build w tym repozytorium generuje nazwy plików z hashem, a potem podaj mi wartości `STATIC_MAX_AGE` i `STATIC_REVALIDATE` dla produkcji i dla środowiska deweloperskiego, i powiedz, co każda z nich zmienia dla powracającej przeglądarki.

Zobacz również

  • Kompresja — jak kompresowalne pliki statyczne są kompresowane i buforowane osobno dla każdego kodowania
  • Routing — jak ścieżki URL są rozwiązywane do plików na dysku
  • Dokumentacja konfiguracji — pełna lista zmiennych środowiskowych
Znalazłeś błąd? Zgłoś go →