Skip to content

[🐞] SSR build emits wrong image asset hashes due to vite-imagetools cache re-encoding #8347

@juanmarin-co

Description

@juanmarin-co

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:

  1. build.client (first Vite build) processes images through vite-imagetools/sharp, writes transformed images to the imagetools cache (node_modules/.cache/imagetools/), and emits assets to dist/assets/ with hash A.
  2. build.server (second Vite build) processes the same images, finds them in the imagetools cache (cache hit), but vite-imagetools loads cached images through sharp() then calls image.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.
  3. The SSR bundle references /assets/{hashB}-image.webp but only /assets/{hashA}-image.webp exists in dist/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:

  1. There is no pre-existing imagetools cache (clean CI environment)
  2. qwik build runs both client and SSR builds sequentially (the normal flow)

The bug is in the dependency vite-imagetools: JonasKruckenberg/imagetools#856

Steps to reproduce

  1. Create a Qwik app with ?jsx image imports
  2. Delete node_modules/.cache/imagetools/ to simulate a clean CI environment
  3. Run qwik build
  4. Compare asset references in server/*.js against files in dist/assets/
  5. 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.5

Additional 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions