Skip to content

perf(image-cache): let the browser cache image previews - #167

Open
czachor wants to merge 1 commit into
artgris:masterfrom
czachor:perf/image-cache
Open

czachor wants to merge 1 commit into
artgris:masterfrom
czachor:perf/image-cache

Conversation

@czachor

@czachor czachor commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Refs #75

Context

Opening the manager on a directory with many images is slow mainly because of the browser: with cachebreaker enabled (the default), every image preview is downloaded again on every page load. This PR lets the browser cache previews and revalidate them cheaply. Server-side listing work is handled in #166.

Why a separate PR

#166 reduces the work the server does to build the listing and does not change anything the browser receives. This PR changes HTTP behavior: preview URLs and responses of the file route. The two are independent: they touch different code paths, each can be reviewed, merged or reverted on its own, and they can be merged in any order.

Changes

1. Cachebreaker uses the file modification time

Cause: FileTypeService::fileIcon() appended ?time=<time()> to every image preview URL. The URL was different on every page load, so the browser never reused a cached preview and downloaded every image again.

Change: preview() passes the file modification time as the cachebreaker. The URL stays the same until the file changes, so the browser can reuse its cached copy; a replaced file still gets a new URL, which is what the cachebreaker is for. fileIcon() now accepts bool|int|null: an int is used as the version, true still falls back to time(), so the Twig fileIcon() function behaves as before.

2. File route answers revalidation with 304

Cause: binaryFileResponseAction (the file_manager_file route, used for previews when the directory is not public) always sent the whole file, even when the browser already had it and asked with If-Modified-Since.

Change: $response->isNotModified($request) turns the response into 304 Not Modified when the file has not changed. BinaryFileResponse already sets Last-Modified, so no new header is computed. ETag is not added on purpose: setAutoEtag() hashes the whole file on every request.

Backward compatibility

No configuration or template changes. fileIcon() keeps its signature for existing callers (true / false / null). The cache visibility of the file route is unchanged (Symfony default).

Tests

  • vendor/bin/simple-phpunit: 11 tests, 30 assertions, OK (Symfony 7.4).
  • New CachebreakerTest with its own environment (config_cachebreaker.yml, cachebreaker: true) and fixture directory Tests/Fixtures/web/images, so existing tests and fixtures are untouched:
    • the preview URL ends with time=<file mtime> (the fixture is touched to a fixed past date, so a time() value cannot match);
    • a request with If-Modified-Since returns 304.

Both new tests fail without the code changes. The branch merges cleanly with #166 (19 tests OK after the merge).

The cachebreaker appended time() to every image preview URL, so the URL
changed on each page load and the browser downloaded every preview again.
preview() now passes the file modification time, which keeps the URL
stable until the file changes. fileIcon() accepts an int version; true
still falls back to time() for Twig fileIcon() callers.

The file route now answers revalidation requests (If-Modified-Since)
with 304 instead of resending the file. Last-Modified was already set
by BinaryFileResponse.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant