html
html copied to clipboard
Failed dynamic import should not always be cached
Given feedback from https://bugs.chromium.org/p/chromium/issues/detail?id=1195405#c9
In my opinion, HTML spec should also stop demanding to cache failed dynamic imports.
The rationale of this proposal is well discussed in https://github.com/tc39/proposal-dynamic-import/issues/80 The ECMAScript spec has adopted the change long ago: https://github.com/tc39/ecma262/pull/1645
Lack of retry-ability is stopping SPA developers from using native dynamic imports. Because they would have to reload the whole web app, only to make up for an otherwise insignificant network failure when fetching a lazy-loaded js chunk.
cc/ @hiroshige-g @domenic
While Safari isn't conforming to the spec in this case, I think their behavior is more intuitive and reasonable to web developers. We should fix the spec, not the browser 🙂
cc @whatwg/modules
If we end up deciding to change behavior here, https://bugzilla.mozilla.org/show_bug.cgi?id=1716019 can be reopened and repurposed.
Some random thoughts (I'm neutral about whether this change should be made or not; focusing its feasibility here):
Should we retry { fetch errors | parse errors | instantiation errors | evaluation errors }?
I prefer just retrying (at most) fetch errors, due to its complexity (I assume fetch errors < parse errors <<<< instantiation errors << evaluation errors
).
Should we retry in {dynamic imports | static imports | <script src>
}?
Perhaps all according to https://github.com/tc39/proposal-dynamic-import/issues/80#issuecomment-512502110.
How it should be specified?
Primarily we should change
https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script
to allow retrying fetch when previous result is null
(by clearing moduleMap[url]
instead of setting it to null
in Step 9, or by only early returning when moduleMap[url] exists AND it is not null
in Step 3), but we also have to check other uses of module maps especially we want to retry parse errors.
In this way, fetch errors are always not retried within a single import()
thanks to visited set
around https://html.spec.whatwg.org/multipage/webappapis.html#internal-module-script-graph-fetching-procedure, may be retried between multiple concurrent import()
s, and are always retried when starting another import()
after previous import()
was rejected (if there are no other module fetches).
Thanks @hiroshige-g!
I believe these are similar thoughts from @domenic's comment (from 2019):
(...) Tentatively we have the following thoughts:
- This should behave the same for static and dynamic imports. For example we should just not cache failed fetches (we currently do). This is important if, for example, you do
import("./a")
where./a
statically imports./b
and./b
has a transient network error. You will want to just retryimport("./a")
.- We are unsure how to treat parse errors. Should we continue caching them? Or should we allow retrying failed parses?
- How this impacts multiple parallel module graph fetches seems complicated.
One thing that I'm imagining here:
to allow retrying fetch when previous result is
null
(by clearingmoduleMap[url]
instead of setting it tonull
in Step 9, or by only early returning whenmoduleMap[url]
exists AND it is notnull
in Step 3)
I believe if Step 3 just says something like: If moduleMap[url] exists and is not null
would also require a change in Step 2, as this one would need to loop back if moduleMap[url]
goes back to "fetching"
. Maybe this is
- If moduleMap[url] is "fetching", wait in parallel until that entry's value changes, then queue a task on the networking task source to proceed with repeating this step.
- If moduleMap[url] exists and is not
null
, asynchronously complete this algorithm with moduleMap[url], and return.
The new Step 3 would have an immediate follow up to change a null moduleMap[url]
to "fetching"
.
Step 2 works today because there is no repeated "fetching"
.
I hope this helps moving this issue forward towards an effective resolution!
Is there anything I can do to move this issue forward?
Any progress?
I'm guessing this hasn't made much progress because there aren't a lot of people clamoring for a solution, so I'm just here to be an additional squeaky wheel. I'm about to "fix" this in my own project by implementing a service worker that will retry only requests for javascript files, which strikes me as an extremely drastic measure to have to take to guard against a common condition like flaky connectivity.
The user experience when this fails is also really, really bad -- parts of the app just entirely fail to load, and as far as I can tell the only mitigation short of the service worker approach would be to report that an error happened and encourage/force the user to reload the page. It's a bad look for the app developer, and I don't think most engineers know that this is a danger when implementing code-splitting, which is part of all the major SPA frameworks and a standard recommendation for large apps. I'm really surprised I haven't heard about this biting more people, and I wonder if a lot of teams either don't know this is happening to them or have given up on solving it.
Same problem. I use microfrontends with chunks. I'm trying to protect against chunk loading error when deploying a new microfrontend version. Because the old chunks are gone. I'm trying to reimport a microfrontend, but it's cached.
I was struggling with this issue and found a workaround. and had the chance to even write about it here: it shows the workaround with react.lazy, but the same mechanism can be applied in any framework or vanilla js. Retry dynamic imports with “React.lazy”
Caching the result of a failed import()
is in-intuitive to say the least. Please change the spec.
I'd like to reiterate the importance of addressing this specification issue. ES6 modules should be the preferred choice for building a large SPA website, but currently result in a poor experience for users with unreliable network connections.
Our use case: We utilize Vite and Rollup to produce ES6 modules for a large-scale website. However, we've faced difficulties with the workarounds provided here:
- Cache-busting query parameters: This approach only works works when the network failure occurs on the dynamically imported file itself. It doesn't work when the network request succeeds for the dynamic import but fails for one of its descendent imports. This scenario is very common in SPAs, where the SPA router dynamically loads the page component code for the current route, but the page component uses static imports for its descendent components.
- Service worker fetch: The network request only occurs the first time the import() is invoked. Subsequent import() calls use the module map, bypassing any potential fetch handling by a service worker.
-
Reloading the page upon
import()
error: This technically "solves" the problem, but is not a good user experience.
I genuinely hope this can be prioritized, as it significantly impacts SPA applications using ES6 modules.
we have the same problem on our projects. we have a different cdn as the backup for the scenario "unstable network" or "DNS cache pollution".
- for the js/css/image resource import with Link/script/img tags we use the package like https://github.com/huajiejin/url-fallback to retry for the backup cdn resource. but the solution not working for the "lazy load module".
Dirty hack to solve it:
import('my-script.js?r=' + Date.now())
@nathnolt this was already covered in:
- Cache-busting query parameters: This approach only works works when the network failure occurs on the dynamically imported file itself. It doesn't work when the network request succeeds for the dynamic import but fails for one of its descendent imports. This scenario is very common in SPAs, where the SPA router dynamically loads the page component code for the current route, but the page component uses static imports for its descendent components.
I see. It's strange that such a common thing like import() is so fragile under non perfect network conditions.
I see. It's strange that such a common thing like import() is so fragile under non perfect network conditions.
It is strange. Dynamic Import is ~7 years old, this issue is ~3 years old, and the same issue was resolved in the ECMAScript specification ~5 years ago.
But it also makes sense! Most of the internet runs on older bundlers like Webpack UMD. As @davidwallacejackson wrote, there aren't enough squeaky wheels.
The good news is that in 2023 Webpack growth began to decline and ESBuild / Rollup / Vite usage is rising exponentially. With luck this issue will soon have enough squeaky wheels to demand action.
@domenic and @hiroshige-g and @annevk it's been a few years since you commented on this issue, but I think it warrants a second look as ESM usage is increasing exponentially. In your previous investigation, you identified that Chrome and Firefox are following this problematic WHATWG specification but Safari is not: https://issues.chromium.org/issues/40759006#comment11
For a real-world example of how this impacts the Chrome and Firefox user experience, I will share some data from my company's website. We have ~750 javascript modules and approximately 10% of imports are done using import()
. We log import()
errors, and they reveal a huge difference between Safari vs. Chrome and Firefox:
- Android Chrome:
- 0.5% of all unique users experience an
import()
error. - 459
import()
errors per 1000 unique users. - Average of 90 errors per unique user who experiences an
import()
error.
- 0.5% of all unique users experience an
- iPhone Safari:
- 1.3% of all unique users experience an
import()
error. - 52
import()
errors per 1000 unique users. - Average of 4.2 errors per unique user who experiences an
import()
error.
- 1.3% of all unique users experience an
- Firefox (all devices excluding bot user agents):
- 2.1% of all unique users experience an
import()
error. - 3340
import()
errors per 1000 unique users. - Average of 160 errors per unique user who experiences an
import()
error.
- 2.1% of all unique users experience an
From this data I conclude that import()
errors are inevitable in real-world network conditions. When the inevitable errors occur, Chrome users get stuck in a repeating error loop even after their network connectivity improves, whereas Safari users are able to recover because Safari is NOT following the current specification.
Edited to include Firefox data. A larger time range was necessary due to lower Firefox traffic on our site, so I also updated Safari and Chrome with data from same time range. The new numbers vary a bit from the original numbers, but clearly show the same trend.
@past I marked this agenda+ mainly to seek feedback on @zcrittendon's request above. @lucacasonato kindly wrote up https://github.com/whatwg/html/pull/10327 so at this point the main question is whether Chromium and Gecko are interested in making a change here. (I won't attend this week due to a holiday, but I don't think I'm needed for this. If you feel otherwise feel free to postpone.)
Amazing! Thanks so much @annevk and @lucacasonato -- really appreciate your attention to this! Hopefully Chromium and Gecko will support this change as it will significantly improve the browsing experience for their users.
@jonco3 @codehag
Hello, is there any progress ?