-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Which component is affected?
Qwik City (routing)
Describe the bug
When qwik build runs the client and SSR Vite builds sequentially, images imported with ?jsx get different content hashes in the SSR bundle vs the client bundle. This causes broken images during server-side rendering — the SSR HTML references asset URLs that don't exist in dist/assets/.
What happens:
build.client(first Vite build) processes images throughvite-imagetools/sharp, writes transformed images to the imagetools cache (node_modules/.cache/imagetools/), and emits assets todist/assets/with hash A.build.server(second Vite build) processes the same images, finds them in the imagetools cache (cache hit), butvite-imagetoolsloads cached images throughsharp()then callsimage.toBuffer()to emit them. This re-encodes the image through sharp's pipeline instead of returning the raw cached bytes, producing different output → hash B.- The SSR bundle references
/assets/{hashB}-image.webpbut only/assets/{hashA}-image.webpexists indist/assets/.
What I expected: SSR and client bundles reference the same asset hashes.
Key finding: The root cause is a bug in vite-imagetools where the cache restore path re-encodes images through sharp instead of using the raw cached bytes. Filed as: JonasKruckenberg/imagetools#856
Why this matters for Qwik specifically: Qwik City's imagePlugin (in @builder.io/qwik-city/lib/vite/index.mjs) integrates vite-imagetools for ?jsx image imports, and qwik build runs two separate Vite builds that both process images through this plugin. This makes Qwik the primary consumer affected by this upstream bug.
Reproduction
No standalone reproduction repo — the issue only manifests when:
- There is no pre-existing imagetools cache (clean CI environment)
qwik buildruns both client and SSR builds sequentially (the normal flow)
The bug is in the dependency vite-imagetools: JonasKruckenberg/imagetools#856
Steps to reproduce
- Create a Qwik app with
?jsximage imports - Delete
node_modules/.cache/imagetools/to simulate a clean CI environment - Run
qwik build - Compare asset references in
server/*.jsagainst files indist/assets/ - Some (or all) image assets referenced by the SSR bundle will not exist in
dist/assets/
Note: this is easier to reproduce on CI (GitHub Actions) where the cache is always cold. Locally, a warm cache from previous builds masks the issue since both builds get cache hits with the same (stale but consistent) re-encoded bytes.
System Info
System:
OS: Linux 6.18 Arch Linux
CPU: (12) x64 Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
Memory: 23.15 GB / 31.20 GB
Binaries:
Node: 24.13.0
pnpm: 10.28.2
npmPackages:
@builder.io/qwik: 1.19.0
@builder.io/qwik-city: 1.19.0
typescript: 5.9.3
vite: 7.3.1
vite-imagetools: 9.0.2
sharp: 0.34.5Additional Information
Workaround: We patched vite-imagetools via pnpm patch to read cached files as raw bytes (readFile) instead of loading them through sharp(). This ensures toBuffer() is never called on cached images, so the emitted bytes are always identical to what was originally cached.
The fix in vite-imagetools is straightforward — on cache hit, use the raw file bytes for emission instead of re-encoding through sharp. See the suggested fix in the upstream issue: JonasKruckenberg/imagetools#856