vite icon indicating copy to clipboard operation
vite copied to clipboard

[build] importing from hashed chunks makes caching terribly ineffective

Open jacekkarczmarczyk opened this issue 3 years ago • 44 comments

Update

I've published a plugin that solves the issue for me: https://github.com/jacekkarczmarczyk/importmap-plugin See https://github.com/vitejs/vite/issues/6773#issuecomment-1308048405 for example usage in the issue reproduction repository

Describe the bug

Built files import other files which names contain content hash. So if the chunk A changes its contents then the output file changes its hash (A.123.js becomes A.234.js). So if there's other file that imports from A chunk then it also changes its contents and hash because import {...} from 'A.123.js' becomes import {...} from 'A.234.js'.

Imagine now that I'm defining an env variable with build time. Main chunk imports this file to show the build time in App.vue. However main chunk exports the following vue related function:

function lc (e, t, r, n, i, a, o, s) {
  var f = typeof e == 'function' ? e.options : e;
  t && (f.render = t, f.staticRenderFns = r, f._compiled = !0), n && (f.functional = !0), a && (f._scopeId = 'data-v-' + a);
  var u;
  if (o ? (u = function (d) {
    d = d || this.$vnode && this.$vnode.ssrContext || this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext, !d && typeof __VUE_SSR_CONTEXT__ != 'undefined' && (d = __VUE_SSR_CONTEXT__), i && i.call(this, d), d && d._registeredComponents && d._registeredComponents.add(o);
...

that is being imported by all component chunks (note that this is just example, depending on the project there might be other user defined functions that are exported from main chunk, also in my actual project I've extracted vendors to separate chunks but still vue related functions were exported from main chunk).

So now when I build again env chunk will change its contents and hash/name, therefore main chunk will change it's contents and hash/name, therefor ALL other chunks will change their names. That makes caching very innefective. Also worth to mention that that technique worked fine in vue-cli - when build time changed only single small env.hash.js chunk was changed, all others remained unchanged (EDIT: there's also relatively small runtime chunk that also was changed)

Reproduction

https://github.com/jacekkarczmarczyk/vite-chunks-very-very-bad

Run yarn && yarn build && git add . && yarn build && git status

It behaves the same when displaying build time is moved to an async Home.vue route component, in this case hash change of main chunk is definitely unjustified

System Info

System:
    OS: Windows 10 10.0.19043
    CPU: (12) x64 Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
    Memory: 3.34 GB / 15.87 GB
  Binaries:
    Node: 16.11.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.10 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 8.0.0 - C:\Program Files\nodejs\npm.CMD
  npmPackages:
    @vitejs/plugin-legacy: ^1.6.4 => 1.6.4
    vite: ^2.7.2 => 2.7.13

Used Package Manager

yarn

Logs

No response

Validations

Additional notes

I believe that this more like rollup issue (or whatever is used for generating chunks) but if it's possible to fix it by using some rollup's settings then vite should use these settings by default. And if it's not possible to fix it by using different settings then I think that chosing rollup was not the best choice. This page https://bundlers.tooling.report/hashing/js-import-cascade/ and this https://bundlers.tooling.report/hashing/avoid-cascade/ however suggest that this should not be an issue for rollup

jacekkarczmarczyk avatar Feb 06 '22 08:02 jacekkarczmarczyk

Additional example 1 (with vue 3)

I've created a new branch in my reproduction so that maybe the problem is clearer:

https://github.com/jacekkarczmarczyk/vite-chunks-very-very-bad/tree/vue3

Run yarn && yarn build && git add ., then change <div>Home</div> to <div>Home2</div> in Home.vue and run yarn build && git status. View1 etc chunks should not be changed but that's what is happening:

image

Additional example 2 (without vue)

https://github.com/jacekkarczmarczyk/vite-chunks-very-very-bad/tree/no-vue

Update body of log() function in util.ts and rebuild. Only util chunk should be changed, but all 4 (index, foo, bar, util) are

Webpack example

https://github.com/jacekkarczmarczyk/vite-chunks-very-very-bad/tree/webpack-async

Change the text in util.js and run yarn build - only util and runtime chunks are changed, foo, bar and main remain the same

jacekkarczmarczyk avatar Feb 06 '22 13:02 jacekkarczmarczyk

I had a very similar problem and came up with this hacky solution:

import { defineConfig } from 'vite'
import { createHash } from 'crypto'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].js',
        chunkFileNames: chunkInfo => {
          if (chunkInfo.isDynamicEntry) {
            const hash = createHash('md5')
              .update(Object.values(chunkInfo.modules).map(m => m.code).join())
              .digest('hex')
              .substr(0, 6)
            return 'assets/[name].' + hash + '.js'
          } else {
            return 'assets/[name].[hash].js'
          }
        }
      }
    }
  }
})

When adding Cache-Control: must-revalidate, max-age=0 for /assets/index.js (my entry file) and Cache-Control: public, max-age=31557600 to all other assets it seems to work fine but I had no chance to really test it yet and I'm quite new to Vite/Rollup and ESM in general so this might be a terrible idea for some reason I don't know...

Every kind of feedback is very appreciated! :)

anatolsommer avatar Mar 03 '22 19:03 anatolsommer

@jacekkarczmarczyk I have very much the same problem due to some (react) lazy component loading. Did you manage to work-around the issue?

redox avatar Jul 10 '22 21:07 redox

Yes, by using Vue Cli

jacekkarczmarczyk avatar Jul 10 '22 22:07 jacekkarczmarczyk

now ,How to configure Vite long-term cache?

1391020381 avatar Oct 26 '22 09:10 1391020381

There is an additional problem with circular dependencies causing all components to import the main index because Vite ejects a helper function there (a virtual module).

Here's how I prevent that helper function (and Vite polyfills) from being added to main index chunk (and keep it lean):

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    target: 'ES2020',
    rollupOptions: {
      output: {
        // Workaround: Vite is bundling its plugins to the main index chunk, 
        // causing circular dependencies and cascading hash changes.
        manualChunks(id) {
          if (id.startsWith('vite/') || id.startsWith('\0vite/')) {
            // Put the Vite modules and virtual modules (beginning with \0) into a vite chunk.
            return 'vite';
          }
        },
      },
    },
  },
  plugins: [vue()]
});

jfparadis-appomni avatar Oct 26 '22 14:10 jfparadis-appomni

@patak-dev sadly, #9870 didn't change much

Update: no improvement with vite 4.0.0-alpha.1 and rollup 3.3.x

Results with Vite 4.0.0-alpha.0

Example 1 (vue 2)

Steps: run yarn && yarn build && git add . && yarn build && git status

Vite 2.9 (main branch)

image

Vite 4.0 (main-v4 branch)

image

Example 2 (Vue 3)

Steps: run yarn && yarn build && git add ., then change <div>Home</div> to <div>Home2</div> in Home.vue and run yarn build && git status

Vite 2.9 (vue3 branch)

image

Vite 4.0 (vue3-vite4 branch)

image

Example 3 (no Vue)

Steps: run yarn && yarn build && git add ., then update body of log() function in util.ts and run yarn build && git status

Vite 2.9 (no-vue branch)

image

Vite 4.0 (no-vue-vite4 branch)

image

jacekkarczmarczyk avatar Nov 07 '22 13:11 jacekkarczmarczyk

I don't think not much can be done here on Vite side (except using another bundler which I don't believe is going to happen soon), I've added the issue to rollup repo

jacekkarczmarczyk avatar Nov 08 '22 18:11 jacekkarczmarczyk

For anyone interested - here's a proof of concept based on @lukastaegert's comment in rollup repo:

https://github.com/jacekkarczmarczyk/importmap-plugin (UPDATE: now supports SystemJS)

Feel free to steal the code and create a proper vite/rollup plugin. Any comments welcome

Plugin applied to the original reproduction (https://github.com/jacekkarczmarczyk/vite-chunks-very-very-bad/tree/main-stable-hash-plugin - yarn && yarn build && git add . && yarn build && git status) - working as expected (only modified chunk changed the hash):

image

jacekkarczmarczyk avatar Nov 09 '22 01:11 jacekkarczmarczyk

vite is fast and great, but this one feature really causes a lot of issues. For example on every update of our application - the dynamic loads all give errors because the bundle they need (even if it did not change) has a hash change - so the file does not exist.

@bluwy - is there plan to fix this caching issue - unsure if the suggestion above is the right fix. Just trying to get some eyes on this to maybe be fixed in 4.1?

waynebrantley avatar Jan 04 '23 21:01 waynebrantley

There really isn't a way to simply fix this as explained in https://github.com/vitejs/vite/issues/6773#issuecomment-1308048405. The way bundlers work without a runtime proxy module loader means it would always have this hash cascading issue. Unless Vite implements it by default, which could be a possibility, but your best bet for now is to use the plugin: https://github.com/jacekkarczmarczyk/importmap-plugin.

bluwy avatar Jan 05 '23 07:01 bluwy

the dynamic loads all give errors because the bundle they need (even if it did not change) has a hash change - so the file does not exist.

This sounds like you need to fix your deployment strategy. My recommendation is to not delete the files from your previous deployment but just put the changed files next to it, that fixes this issue easily. Otherwise, there is no safe way to do a deployment without compromising long-running sessions. Another approach is not to use hashes but separate folders per deployment.

lukastaegert avatar Jan 05 '23 09:01 lukastaegert

@lukastaegert We do an automated deployment (OctopusDeploy) that contains the package of the site (client and server). This creates a brand new deployment, folder, etc with the files from the deployment (which would not contain old files by definition). This has the positive thing of being a brand new deployment with all new configs and such - but has the negative of old files not being there. With create-react-app (what we used before), this type of error occurred very infrequently (like once per month type of thing) instead of every deployment.

I agree we would compromise long-running sessions when the code they were needing changed. However, we bundle large dependencies separately - so rarely do the actually change - but with vite - appears they always change.

@bluwy thank you for the comment and consideration. I was just 'waiting for official fix', but will try above. Perhaps consider adding this side-effect to the docs in the meantime? maybe https://vitejs.dev/guide/features.html#async-chunk-loading-optimization and https://vitejs.dev/guide/build.html#chunking-strategy letting people that ViteJs will generate a new hash each time for dynamically loaded modules even if they did not change (resulting in a new filename each time) - eliminating the browser cache feature of generating 'stable chunks'.

Thanks again for a great product - our build times have never been faster!

waynebrantley avatar Jan 05 '23 21:01 waynebrantley

Unless Vite implements it by default, which could be a possibility

@bluwy can I ask how high this possibility is? Is it something closer to "yeah, maybe, after we solve 1000 of other more important issues" or to "that's indeed a big problem we want to solve asap" (regardless of whether it's easy or hard to implement it).

@waynebrantley while my plugin seems to solve the problem in simple cases and I do use it in production with smaller projects, it wasn't tested (and I'm pretty sure it wouldn't work) in more complicated setups (like PWA or anything that relies on chunk names), so I'd prefer to see it as a built-in solution instead of 3rd party plugin.

Not sure if it needs some changes in rollup to make the change in Vite easier though, there doesn't seem to be much discussion on it in rollup repo (some of other discussions are linked in this issue), which surprises me a lot tbh as I don't believe that most of the projects are "release and forget" or that devs don't case about caching...

jacekkarczmarczyk avatar Jan 06 '23 17:01 jacekkarczmarczyk

@bluwy can I ask how high this possibility is? Is it something closer to "yeah, maybe, after we solve 1000 of other more important issues" or to "that's indeed a big problem we want to solve asap" (regardless of whether it's easy or hard to implement it).

It's more of "if someone's interested and creates a PR, we can discuss and review it" kinda situation. Since Vite's development comprises of community contributions, and sometimes sponsored work from metaframeworks, we don't have a list of things we have to do. For me, I usually implement features or bug fixes if I find them interesting.

bluwy avatar Jan 09 '23 04:01 bluwy

@patak-dev sadly, #9870 didn't change much

Update: no improvement with vite 4.0.0-alpha.1 and rollup 3.3.x

Results with Vite 4.0.0-alpha.0

Example 1 (vue 2)

Steps: run yarn && yarn build && git add . && yarn build && git status

Vite 2.9 (main branch)

image

Vite 4.0 (main-v4 branch)

image

Example 2 (Vue 3)

Steps: run yarn && yarn build && git add ., then change <div>Home</div> to <div>Home2</div> in Home.vue and run yarn build && git status

Vite 2.9 (vue3 branch)

image

Vite 4.0 (vue3-vite4 branch)

image

Example 3 (no Vue)

Steps: run yarn && yarn build && git add ., then update body of log() function in util.ts and run yarn build && git status

Vite 2.9 (no-vue branch)

image

Vite 4.0 (no-vue-vite4 branch)

image

I meet same question and update [email protected],but not work,How did you solve it?

JaweenDeng avatar Feb 01 '23 07:02 JaweenDeng

I like the approach that parcel uses to resolve this issue

https://parceljs.org/features/production/#cascading-invalidation

yeion7 avatar Feb 06 '23 19:02 yeion7

This is not straightforward as it requires you to have an intermediate loading handler that maps imports to their hashed counterparts when something is imported. That being said, this is possible by using import maps. While those are not yet widely adopted, this makes them work for at least 95% of browsers: https://github.com/guybedford/es-module-shims

Then you can basically completely remove hashes from file names and instead e.g. put them into import maps like this:

<script type="importmap">
{
  "imports": {
    "./my-chunk.js": "./my-chunk.js?ab34fa7"
  }
}
</script>

Here, ab34fa7 would be the content hash of ./my-chunk.js. This makes sure that whenever the content hash is changed, this specific chunk will be loaded again. Of course, your server needs to ignore the query parameter, but this is usually the case out of the box.

It should be easy to put this into a plugin, but I would not be surprised if someone has not done that already. It will completely avoid any hash cascades.

Note that it will not completely fix the original issue because long-running sessions through a re-deploy will still run into chunks that e.g. may be missing imports, or chunks that do not exist at all. I.e. chunk foo.js may have an import a before deployment, but an import b after deployment. If a long-running sessions through a re-deployment now triggers a dynamic import of foo.js expecting it to have an import a, it will likely crash with a runtime error. This would be avoided if you used content hashes on the actual files and deployed the previous deployment with the current deployment.

To fix that while preventing the hashing cascade, one could spin it differently:

All files have content hashes, but internally, they reference other chunks without hashes. To do that, one would need to create a bundle without hashes and in a post-processing step, calculate the content hashes of each chunk and change the file names and generate the import map.

<script type="importmap">
{
  "imports": {
    "./my-chunk.js": "./my-chunk-ab34fa7.js"
  }
}
</script>

lukastaegert avatar Feb 07 '23 09:02 lukastaegert

It should be easy to put this into a plugin, but I would not be surprised if someone has not done that already

@lukastaegert Someone did, and it's already mentioned few times in this issue

jacekkarczmarczyk avatar Feb 07 '23 09:02 jacekkarczmarczyk

I had a very similar problem and came up with this hacky solution:

import { defineConfig } from 'vite'
import { createHash } from 'crypto'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].js',
        chunkFileNames: chunkInfo => {
          if (chunkInfo.isDynamicEntry) {
            const hash = createHash('md5')
              .update(Object.values(chunkInfo.modules).map(m => m.code).join())
              .digest('hex')
              .substr(0, 6)
            return 'assets/[name].' + hash + '.js'
          } else {
            return 'assets/[name].[hash].js'
          }
        }
      }
    }
  }
})

When adding Cache-Control: must-revalidate, max-age=0 for /assets/index.js (my entry file) and Cache-Control: public, max-age=31557600 to all other assets it seems to work fine but I had no chance to really test it yet and I'm quite new to Vite/Rollup and ESM in general so this might be a terrible idea for some reason I don't know...

Every kind of feedback is very appreciated! :)

this does not work with vite 4.x, the chunkInfo has no member named modules

ijandc avatar Mar 17 '23 05:03 ijandc

I had a very similar problem and came up with this hacky solution:

import { defineConfig } from 'vite'
import { createHash } from 'crypto'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].js',
        chunkFileNames: chunkInfo => {
          if (chunkInfo.isDynamicEntry) {
            const hash = createHash('md5')
              .update(Object.values(chunkInfo.modules).map(m => m.code).join())
              .digest('hex')
              .substr(0, 6)
            return 'assets/[name].' + hash + '.js'
          } else {
            return 'assets/[name].[hash].js'
          }
        }
      }
    }
  }
})

When adding Cache-Control: must-revalidate, max-age=0 for /assets/index.js (my entry file) and Cache-Control: public, max-age=31557600 to all other assets it seems to work fine but I had no chance to really test it yet and I'm quite new to Vite/Rollup and ESM in general so this might be a terrible idea for some reason I don't know... Every kind of feedback is very appreciated! :)

this does not work with vite 4.x, the chunkInfo has no member named modules

Did you find a solution for v4?

volkandkaya avatar Mar 24 '23 04:03 volkandkaya

@ijandc + @volkandkaya As it seems the new Rollup version that Vite 4 uses, solved the problem and now the entryFileNames: 'assets/[name].js' part alone (without chunkFileNames and the extra hashing) works quite fine.

anatolsommer avatar Apr 16 '23 17:04 anatolsommer

Is there any progress?

0x30 avatar Jul 13 '23 13:07 0x30

This problem also affects us, webpack uses contenthash, but it seems vite has no corresponding solution?

dukexie avatar Jul 15 '23 05:07 dukexie

Pretty sure this would cut my build time down by half due to Monaco Editor being included everytime.

volkandkaya avatar Jul 15 '23 05:07 volkandkaya

占个位 等解决方案

yaozoo avatar Jul 19 '23 09:07 yaozoo

关注了,等个官方解决方案

regaliastar avatar Jul 21 '23 03:07 regaliastar

This problem also affects us, webpack uses contenthash, but it seems vite has no corresponding solution?

ruoweiys14 avatar Jul 27 '23 10:07 ruoweiys14

Since I got 8 downvotes: Seems like I don't get the problem (which is weird since people seemed to like my solution for Vite 2), it works perfectly fine for me. Multiple projects, many builds. As long as this is still about client side caching - no idea what that would have to do with build time.

image (One component changed, every chunk except the one containing that code and the entry file stays unchanged.)

anatolsommer avatar Jul 31 '23 12:07 anatolsommer

@anatolsommer Yes, your screenshot above is awesome. I also very much hope that Vite 4 can also achieve this effect. I think the above 8 downvotes are for entryFileNames: 'assets/[name].js' this one. Because there is still a need for [hash]. Otherwise there will be no cached value.

dukexie avatar Aug 01 '23 01:08 dukexie