Skip to content

svelte.config.js version changes cause most client chunks to generate a new hash despite no other changes #12260

Description

@rory-orennia

Updated bug report

Changing the version in svelte.config.js and rebuilding the exact same code results in most client chunks also being updated with a new hash. The cause seems to be the version check code being updated to look for a new version, and then most client chunks directly or indirectly importing that change.

Initial bug report

Describe the bug

If you build the same project twice, you get different files out even though nothing has changed.

I was trying to break my project up using manualChunks to create a vendor.js file with the hope that while we're deploying multiple times a day, our dependencies rarely change so clients would reuse many of the same js files they'd already have cached from previous visits. In practice this doesn't work because some base level thing that many pieces inherit from is constantly changing so you never get the same vendor.js file out between builds.

The issue seems to be around a randomly generated key in the output like globalThis.__sveltekit_zp1tl where that hash changes every build. That shows up as the main difference in entry.{hash}.js which many other files then import.
image
image

I thought this should work since Richard Harris actually raised this bug with Vite: vitejs/vite#11911

Reproduction

  1. pnpm create svelte@latest deterministic-build
  2. press enter a bunch. none of these settings matter. Same issue appears in Typescript vs JS, adding eslint, etc, or even doing it in npm instead of pnpm
  3. cd deterministic-build
  4. pnpm i
  5. pnpm build. copy output
  6. pnpm build. copy output
  7. compare outputs. Notice some files change

Logs

> deterministic-build@0.0.1 build
> vite build

vite v5.2.11 building SSR bundle for production...
"confetti" is imported from external module "@neoconfetti/svelte" but never used in "src/routes/sverdle/+page.svelte".
✓ 109 modules transformed.
vite v5.2.11 building for production...
✓ 92 modules transformed.
.svelte-kit/output/client/_app/version.json                                                         0.03 kB │ gzip:  0.05 kB
.svelte-kit/output/client/.vite/manifest.json                                                       7.12 kB │ gzip:  0.99 kB
.svelte-kit/output/client/_app/immutable/assets/fira-mono-greek-ext-400-normal.CsqI23CO.woff2       7.51 kB
.svelte-kit/output/client/_app/immutable/assets/fira-mono-cyrillic-400-normal.36-45Uyg.woff2        9.10 kB
.svelte-kit/output/client/_app/immutable/assets/fira-mono-greek-400-normal.C3zng6O6.woff2          10.52 kB
.svelte-kit/output/client/_app/immutable/assets/fira-mono-latin-ext-400-normal.D6XfiR-_.woff2      11.36 kB
.svelte-kit/output/client/_app/immutable/assets/fira-mono-cyrillic-ext-400-normal.B04YIrm4.woff2   15.77 kB
.svelte-kit/output/client/_app/immutable/assets/fira-mono-latin-400-normal.DKjLVgQi.woff2          16.28 kB
.svelte-kit/output/client/_app/immutable/assets/fira-mono-all-400-normal.B2mvLtSD.woff             77.36 kB
.svelte-kit/output/client/_app/immutable/assets/svelte-welcome.0pIiHnVF.webp                      115.47 kB
.svelte-kit/output/client/_app/immutable/assets/svelte-welcome.VNiyy3gC.png                       360.81 kB
.svelte-kit/output/client/_app/immutable/assets/5.CU6psp88.css                                      0.80 kB │ gzip:  0.35 kB
.svelte-kit/output/client/_app/immutable/assets/2.Cs8KR-Bb.css                                      1.47 kB │ gzip:  0.53 kB
.svelte-kit/output/client/_app/immutable/assets/4.DOkkq0IA.css                                      3.78 kB │ gzip:  1.06 kB
.svelte-kit/output/client/_app/immutable/assets/0.CT0x_Q5c.css                                      5.15 kB │ gzip:  1.66 kB
.svelte-kit/output/client/_app/immutable/chunks/index.R8ovVqwX.js                                   0.03 kB │ gzip:  0.05 kB
.svelte-kit/output/client/_app/immutable/entry/start.kuqVoxG5.js                                    0.07 kB │ gzip:  0.08 kB
.svelte-kit/output/client/_app/immutable/chunks/stores.CyV73Wv9.js                                  0.23 kB │ gzip:  0.17 kB
.svelte-kit/output/client/_app/immutable/chunks/index.Ice1EKvx.js                                   0.51 kB │ gzip:  0.34 kB
.svelte-kit/output/client/_app/immutable/nodes/1.DgAi-rhN.js                                        0.84 kB │ gzip:  0.52 kB
.svelte-kit/output/client/_app/immutable/nodes/3.BqQOub2U.js                                        1.57 kB │ gzip:  0.96 kB
.svelte-kit/output/client/_app/immutable/chunks/scheduler.Dk-snqIU.js                               2.25 kB │ gzip:  1.05 kB
.svelte-kit/output/client/_app/immutable/nodes/5.CwxmUzn6.js                                        2.35 kB │ gzip:  1.11 kB
.svelte-kit/output/client/_app/immutable/nodes/2.BMQFqo-e.js                                        5.48 kB │ gzip:  2.60 kB
.svelte-kit/output/client/_app/immutable/chunks/index.DDRweiI9.js                                   6.09 kB │ gzip:  2.57 kB
.svelte-kit/output/client/_app/immutable/entry/app.D6zuvAPi.js                                      6.64 kB │ gzip:  2.65 kB
.svelte-kit/output/client/_app/immutable/nodes/0.qacZO6So.js                                        8.77 kB │ gzip:  3.57 kB
.svelte-kit/output/client/_app/immutable/nodes/4.D_o_hs5U.js                                       17.08 kB │ gzip:  7.07 kB
.svelte-kit/output/client/_app/immutable/chunks/entry.DI0Mx0P3.js                                  27.97 kB │ gzip: 10.97 kB
✓ built in 248ms
.svelte-kit/output/server/.vite/manifest.json                                                       7.17 kB
.svelte-kit/output/server/_app/immutable/assets/fira-mono-greek-ext-400-normal.CsqI23CO.woff2       7.51 kB
.svelte-kit/output/server/_app/immutable/assets/fira-mono-cyrillic-400-normal.36-45Uyg.woff2        9.10 kB
.svelte-kit/output/server/_app/immutable/assets/fira-mono-greek-400-normal.C3zng6O6.woff2          10.52 kB
.svelte-kit/output/server/_app/immutable/assets/fira-mono-latin-ext-400-normal.D6XfiR-_.woff2      11.36 kB
.svelte-kit/output/server/_app/immutable/assets/fira-mono-cyrillic-ext-400-normal.B04YIrm4.woff2   15.77 kB
.svelte-kit/output/server/_app/immutable/assets/fira-mono-latin-400-normal.DKjLVgQi.woff2          16.28 kB
.svelte-kit/output/server/_app/immutable/assets/fira-mono-all-400-normal.B2mvLtSD.woff             77.36 kB
.svelte-kit/output/server/_app/immutable/assets/svelte-welcome.0pIiHnVF.webp                      115.47 kB
.svelte-kit/output/server/_app/immutable/assets/svelte-welcome.VNiyy3gC.png                       360.81 kB
.svelte-kit/output/server/_app/immutable/assets/_page.CU6psp88.css                                  0.80 kB
.svelte-kit/output/server/_app/immutable/assets/_page.BBloWWo2.css                                  1.45 kB
.svelte-kit/output/server/_app/immutable/assets/_page.DOkkq0IA.css                                  3.78 kB
.svelte-kit/output/server/_app/immutable/assets/_layout.c5uvFHuT.css                                5.40 kB
.svelte-kit/output/server/chunks/prod-ssr.js                                                        0.04 kB
.svelte-kit/output/server/entries/pages/_page.ts.js                                                 0.05 kB
.svelte-kit/output/server/chunks/index3.js                                                          0.08 kB
.svelte-kit/output/server/entries/pages/about/_page.ts.js                                           0.13 kB
.svelte-kit/output/server/entries/pages/sverdle/how-to-play/_page.ts.js                             0.13 kB
.svelte-kit/output/server/chunks/client.js                                                          0.28 kB
.svelte-kit/output/server/internal.js                                                               0.31 kB
.svelte-kit/output/server/entries/fallbacks/error.svelte.js                                         0.47 kB
.svelte-kit/output/server/chunks/stores.js                                                          0.54 kB
.svelte-kit/output/server/entries/pages/about/_page.svelte.js                                       1.07 kB
.svelte-kit/output/server/chunks/index2.js                                                          1.33 kB
.svelte-kit/output/server/chunks/index.js                                                           2.10 kB
.svelte-kit/output/server/chunks/ssr.js                                                             4.00 kB
.svelte-kit/output/server/chunks/exports.js                                                         5.96 kB
.svelte-kit/output/server/chunks/internal.js                                                        6.03 kB
.svelte-kit/output/server/entries/pages/sverdle/how-to-play/_page.svelte.js                         6.47 kB
.svelte-kit/output/server/entries/pages/_page.svelte.js                                            13.48 kB
.svelte-kit/output/server/entries/pages/_layout.svelte.js                                          14.67 kB
.svelte-kit/output/server/entries/pages/sverdle/_page.svelte.js                                    23.42 kB
.svelte-kit/output/server/index.js                                                                 90.30 kB
.svelte-kit/output/server/entries/pages/sverdle/_page.server.ts.js                                146.12 kB
✓ built in 1.01s

Run npm run preview to preview your production build locally.

> Using @sveltejs/adapter-auto
  Could not detect a supported production environment. See https://kit.svelte.dev/docs/adapters to learn how to configure your app to run on the platform of your choosing
  ✔ done

System Info

System:
    OS: macOS 14.4.1
    CPU: (16) arm64 Apple M3 Max
    Memory: 2.59 GB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 21.6.0 - /usr/local/bin/node
    npm: 10.3.0 - /usr/local/bin/npm
    pnpm: 9.1.0 - ~/Library/pnpm/pnpm
  Browsers:
    Chrome: 125.0.6422.78
    Safari: 17.4.1
  npmPackages:
    @sveltejs/adapter-auto: ^3.0.0 => 3.2.1
    @sveltejs/kit: ^2.0.0 => 2.5.10
    @sveltejs/vite-plugin-svelte: ^3.0.0 => 3.1.0
    svelte: ^4.2.7 => 4.2.17
    vite: ^5.0.3 => 5.2.11

Severity

serious, but I can work around it

Additional Information

No response

Activity

  1. Conduitry commented on May 24, 2024

    @Conduitry
    Member

    You need to set https://kit.svelte.dev/docs/configuration#version to something consistent; otherwise, it uses the current timestamp, which changes every build, affecting the hashes.

  2. rory-orennia commented on May 24, 2024

    @rory-orennia
    Author

    @Conduitry Ohh thank you! That was definitely it. I guess now my concern is, how is this supposed to work in the case of making a vendor.js file? Unless I hardcode the version to something and then ignore the svelte version handling, I'm still going to get a different vendor.js file every time I do a new commit (assuming you tie version to git like is suggested).
    I think even if I manually pulled this version check out into another chunk, it would be referenced by most other client chunks in the app and thus they would also change their hash.

  3. rory-orennia commented on May 24, 2024

    @rory-orennia
    Author

    Here's me changing the svelte.config.js -> version -> name from 1 to 2 and it basically changing every client chunk
    image

  4. mquandalle commented on May 27, 2024

    @mquandalle

    If two compiled files across two distincts app versions are exactly the same, why not keep the same file name to avoid unnecessary cache invalidation on the client?

  5. rory-orennia commented on May 27, 2024

    @rory-orennia
    Author

    If two compiled files across two distincts app versions are exactly the same, why not keep the same file name to avoid unnecessary cache invalidation on the client?

    That's the problem. They aren't exactly the same because some base level sveltekit code gets changed, and then most client code chunks import that change, which causes them to change because the hash they reference (eg. import somebasefile.constantlychanginghash.js) has changed.

    If the cause of all of this is the version check code, I think we need to find a way to make that code not bleed into every client chunk

  6. rory-orennia commented on May 27, 2024

    @rory-orennia
    Author

    @eltigerchino @Conduitry Do you want me to close this ticket and open a new one that is the new problem? (Version changes cause most client chunks to generate a new hash despite no other changes). Or do you want me to rename this one instead?

  7. teemingc commented on May 27, 2024

    @teemingc
    Member

    @eltigerchino @Conduitry Do you want me to close this ticket and open a new one that is the new problem? (Version changes cause most client chunks to generate a new hash despite no other changes). Or do you want me to rename this one instead?

    Yes, renaming this one would be great. Thanks!

  8. changed the title [-]SvelteKit builds are non-deterministic[/-] [+]svelte.config.js version changes cause most client chunks to generate a new hash despite no other changes[/+] on May 27, 2024
  9. matindow commented on Jun 15, 2024

    @matindow

    Are there any proposals/thoughts for how to address this?

    How much worse would it be if the content in version.json were replaced with something like a junction map of component names => file paths that all of the chunks referenced dynamically? I am assuming there are a lot of really great/important benefits to being able to hardcode a static file import into all of the chunks that make this a non starter?

    I'm trying to think through other ways of doing this version check, and everything seems to come back to the same frictions.

    I do agree that incrementing the version number should intuitively not cascade into a new hash for unchanged files in your project, though. Seems not great even aside from the terrible caching issues.

  10. jippi commented on Jun 20, 2026

    @jippi

    This is a solution I came up with and running as experiment - summary by Claude because its 1am and I'm le tired

    1. Constant sentinel for kit.version.name

    // svelte.config.js
    const APP_VERSION = '__APP_VERSION__' // constant sentinel, NEVER the SHA
    kit: {
      version: { name: APP_VERSION, pollInterval: 60_000 },
    }

    Now version_hash is constant → globalThis.__sveltekit_<hash> is identical every deploy, and the bundled version string is constant → immutable chunks are byte-stable.

    2. Override the client-only version define to read a <meta> at runtime

    The bundled version must reflect the real build at runtime without baking the SHA. Override the client define so it reads a <meta> tag (constant code → no SHA in bytes):

    // vite.config.ts — a plugin AFTER sveltekit() in the plugins array
    {
      name: 'app-version-runtime',
      configEnvironment(name) {
        if (name !== 'client') return null
        return {
          define: {
            __SVELTEKIT_APP_VERSION__:
              `(typeof document!=='undefined'?(document.querySelector('meta[name="app-version"]')?.getAttribute('content')??''):'')`,
          },
        }
      },
    }

    $app/environment's version (and thus kit's updated check, your analytics, etc.) is now the real SHA at runtime, read once at module-eval from the <meta> parsed in <head> before any module script runs. Leave the server define alone.

    3. Inject the real SHA into that <meta> per request

    <!-- app.html, in <head> (before any module script). No comment with the marker in it — see gotcha. -->
    <meta name="app-version" content="%app_version%" />
    // a `handle` hook
    export const version_inject: Handle = ({ event, resolve }) => {
    	const sha = process.env.GIT_SHA || '__APP_VERSION__' // sentinel fallback when unset (local)
    	return resolve(event, {
    		transformPageChunk: ({ html }) => html.replaceAll('%app_version%', sha), // replaceAll, not replace
    	})
    }

    A <meta>, not an inline <script>: a script injected via transformPageChunk runs after SvelteKit computed its CSP nonce, so a strict script-src would block it; <meta> is not governed by script-src.

    4. Swap the sentinel → real SHA in the two NON-immutable outputs

    _app/version.json (the updated poll target) and service-worker.js (precache-${version}) carry the sentinel and DO need the real SHA. Both are served no-cache / revalidated — not immutable — so rewriting them is safe. Do it in a Vite closeBundle plugin (so vite build ≡ pnpm build — no separate post-build step to forget), gated on the SSR pass so it runs after the adapter assembles build/:

    // vite.config.ts plugin, ordered after sveltekit()
    let resolved
    {
      name: 'app-version-finalize',
      configResolved(c) { resolved = c },
      closeBundle: {
        order: 'post', sequential: true,
        handler() {
          if (!resolved?.build.ssr) return            // mirrors kit's adapter gating
          const sha = process.env.GIT_SHA
          if (!sha) return                            // local: leave the sentinel
          for (const f of ['build/client/_app/version.json', 'build/client/service-worker.js']) {
            writeFileSync(f, readFileSync(f, 'utf8').replaceAll('__APP_VERSION__', sha))
          }
          // guard: assert the SHA never appears under build/client/_app/immutable/**
        }
      }
    }

    Never touch build/client/_app/immutable/** — keeping the SHA out of those bytes is the entire point.

    Why this is correct

    • Built-in updated/reload-on-deploy keeps working, unchanged: the polled version.json (real SHA) is compared against the client version (real SHA from the <meta>) — real-vs-real. After a new deploy they differ → updated.current flips → reload on next nav. No custom detector needed.
    • Immune to the Revert "fix: replace version in generateBundle (#12700)" #12779 class by construction: no immutable byte ever changes. Filenames stay stable because the content is genuinely constant, not because we froze a name over drifting content.
    • Service-worker precache rotates per deploy (its file is re-fetched, not immutable); analytics version tagging stays real.

    Verify

    GIT_SHA=aaaaaaa pnpm build && find build/client/_app/immutable -type f | sed 's|.*/immutable/||' | sort > a.txt
    GIT_SHA=bbbbbbb pnpm build && find build/client/_app/immutable -type f | sed 's|.*/immutable/||' | sort > b.txt
    comm -3 a.txt b.txt   # expect: empty (only version.json + service-worker.js differ)

    In the browser: the <meta> carries the real SHA; version.json matches it; deploying a new build triggers exactly one reload-on-next-nav with no reload loop.

    Gotchas (each one bit us)

    • replaceAll, not replace. String.replace(str, …) swaps only the first occurrence. If anything else on the page contains the marker (e.g. an explanatory HTML comment above the <meta>), it absorbs the swap and the real attribute stays raw → the client reads the literal "%app_version%" → permanent version.json != "%app_version%" mismatch → reload on every navigation. Use replaceAll and don't put the marker in a comment.
    • Scope the define override to the client environment. The server importing version server-side would otherwise read document (undefined).
    • CSP: deliver via <meta>, not an injected inline <script>.

    Applicability

    Single SvelteKit app (no multi-app embedding on one page — a constant globalThis.__sveltekit_<hash> namespace is only a collision risk there). Tested with rolldown-vite; the same shape works on Rollup-Vite. Until SvelteKit ships a first-class fix for #12260, this keeps your immutable assets genuinely immutable across deploys.

  11. michael-haberzettel commented on Jun 21, 2026

    @michael-haberzettel

    @jippi

    Du you tried to check the preload mechanism: https://svelte.dev/tutorial/kit/preload

    Some of my attempts failed because of this system.

    To test it, you can do these steps:

    • run build, preview and display a page in a browser with preloadable links without hovering them to not trigger mechanism.
    • stop the preview server and bring a change to page you want to navigate with preload.
    • run build, preview and go to your previous page, opened in the first step. Are you able to navigate to your preloadable link without errors ?
  12. jippi commented on Jun 21, 2026

    @jippi

    @michael-haberzettel I use the preload system extensively ( https://mangabaka.org/ ) and haven't seen any errors - since my patch above doesn't change anything in svelte, other than the "source of the version value" it shouldn't really impact any behavior in svelte either - at least i haven't noticed anything the last 8-12 development hours since I put this online

  13. michael-haberzettel commented on Jun 21, 2026

    @michael-haberzettel

    I will give it a try on next days 👍

  14. added a commit that references this issue on Jul 14, 2026
    ab5c253
  15. teemingc commented on Jul 16, 2026

    @teemingc
    Member

    Closed by #16324

  16. added theissue type on Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions