Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IE10 and IE11 download both bundles #1

Open
ralscha opened this issue Sep 19, 2017 · 48 comments
Open

IE10 and IE11 download both bundles #1

ralscha opened this issue Sep 19, 2017 · 48 comments

Comments

@ralscha
Copy link

ralscha commented Sep 19, 2017

Hi

IE10 and IE11 download both bundles but only execute the ES2015 bundle and everything works fine.
I'm wondering if there is a clever way to prevent older browsers from downloading the ES2015+ bundle.

Ralph

2017-09-19 07_03_24-ie10 - win7 running - oracle vm virtualbox

2017-09-19 07_05_22-webpack es next boilerplate - internet explorer

@philipwalton
Copy link
Owner

Yeah, this actually happens in Firefox <54 and Safari <11, and current Edge as well, which is unfortunate.

I'm not aware of a way to prevent the download, so I think the decision re: whether to use this technique (or not) needs to be up to individual site owners and based on actual usage data.

Here's how I think about it:

Most of the time desktop users are not network-constrained, so for FF and IE, I'm not terribly worried about the extra download. On mobile this is currently a bigger issue, but more and more Android devices are running modern Chrome and a new mobile Safari will ship with iOS 11 in a few days (and most likely fix the nomodule bug), so the mobile concern is rapidly diminishing.

But even at the moment with a double-download on mobile, keep in mind download size is not the only concern. On mobile devices with slower processors parse/eval time is also a major bottleneck, and there may be cases where downloading more but parsing and eval-ing less can still be a performance win since downloads can happen in parallel (especially when using http/2) but execution on the main thread cannot.

Of course this will vary from site to site, so it's best to test it on your own codebase. On my personal site, more than 75% of my visitors are on Chrome (and most of them desktop), so for me it's definitely a net reduction in both total download time and total parse/eval time.

@philipwalton
Copy link
Owner

philipwalton commented Sep 19, 2017

Update: actually it looks like Safari 11 shipped on iOS today. I tested and confirmed the nomodule bug is fixed!

@ralscha
Copy link
Author

ralscha commented Sep 20, 2017

Thanks for the comprehensive answer.

The implementations in the Windows browsers varies.

Chrome does not download the nomodule bundle.

Firefox 55 has nomodule support (https://bugzilla.mozilla.org/show_bug.cgi?id=1330900) but still downloads both bundles. The issue is tracked in this bugzilla report: https://bugzilla.mozilla.org/show_bug.cgi?id=1382020
And Firefox 55 and 57 not only download but also execute the legacy code.

Edge 15 downloads both bundles and executes the legacy code.
Edge 16, released with the Windows Fall Creator Update later this year, downloads both bundles but does not execute the legacy bundle

@philipwalton
Copy link
Owner

The shim mentioned in the article prevents browsers with <script type="module"> support from executing the code loaded via <script nomodule>, so that shouldn't be a problem (and sites will work as expected).

@ralscha
Copy link
Author

ralscha commented Sep 22, 2017

The shim only works in Safari because onbeforeload is a proprietary feature.

@philipwalton
Copy link
Owner

Ahh, I stand corrected about the shim working for other browsers. But either way, in my testing I've never been able to get FF to execute both bundles. Do you have a demo to reproduce the issue?

If I run this boilerplate and visit the demo page in FF 57 and FF Developer Edition, with the module flag either enabled or disabled, I only ever see the code executed once.

@ralscha
Copy link
Author

ralscha commented Oct 2, 2017

Sorry for the misunderstanding. Firefox does not execute both bundles. He downloads both but then executes the legacy bundle.
But I see my mistake. Module support is not enabled by default in FF55 and FF56. I have to enable module support on the about:config page (dom.moduleScripts.enabled).
When I enable this option Firefox stil downloads both bundles but he only executes
the module bundle.

@pavelloz
Copy link

Hmm. I've observed above behavior as well and for the past days I've been thinking about temporary workaround.

What if we generated es5 (lets call it legacy) webpack runtime that will load async. legacy/es6 build based on some condition (ie. detect module feature)?

I know that it will generate performance hit (especially if this runtime will not be inlined), but with preload we can minimize it pretty well.

Tell me what you think, because I think the idea of serving modern JS today is worth exploring for both, developers and users.

@ralscha
Copy link
Author

ralscha commented Oct 12, 2017

Personally I'm only interested in the mobile platforms and both Safari and Chrome behave as expected. They only download and run the module bundle

But a temporary workaround for Firefox and Edge would be nice.

@ralscha
Copy link
Author

ralscha commented Oct 12, 2017

Just watched Justin Willis video about Stencil.js.
At 52:26 he talks about modules and loading ES6 stencil components this way.
Will be interesting to see how they tackle this problem.

@thebuilder
Copy link

thebuilder commented Oct 16, 2017

Just an idea for solving this. Needs to be fleshed out more, and it will of course require script execution. Could be inlined in HTML, or loaded as a small bootstrap file. You'd also most likely need to load more than one script.

bootstrap.js

const appScript = document.createElement('script')

if (appScript.noModule === false) {
  // If `noModule` is defined on the script tag, the browser should support Modules
  appScript.setAttribute('src', process.env.MODULE_SRC)
} else {
  // Otherwise load the legacy src
  appScript.setAttribute('src', process.env.LEGACY_SRC)
}

appScript.setAttribute('defer', true)
document.body.appendChild(appScript);

@pavelloz
Copy link

@thebuilder
What kind of magic allows us to use process.env in the browser? That looks useful :)

@thebuilder
Copy link

@pavelloz well nothing i guess. Just one way of how you could include the compiled asset path, into another file before compiling it. You would want to use the output from Assets or Manifest Plugin to get the correct filename.

@philipwalton
Copy link
Owner

@thebuilder, the problem with any kind of imperative approach like this is it delays fetching of the file until after your code has run. Such a delay would be worse for performance on module-supporting browsers, and I'd argue cost more than it saves.

@thebuilder
Copy link

You just can't win.
Would it have a delay if added in the <head> as inline script? Would be executed before the parser reaches the script blocks at the end of body.
Could it work if the type="module" scripts are always added, and you use the inline script block to check for modules support before adding the legacy scripts?

@philipwalton
Copy link
Owner

Would it have a delay if added in the as inline script? Would be executed before the parser reaches the script blocks at the end of body.

In addition to the HTML parser, all modern browsers have a preload scanner which looks ahead for resources it can begin fetching early. While in theory a preload scanner could detect URLs in scripts, I don't think any of them do today, so not having it in an HTML element will delay the start of the fetch.

@rosenfeld
Copy link

@thebuilder this is not good for Firefox (latest), for example, since it supports modern features well but script.noModule is undefined by default (unless you explicitly enable it).

I decided to add the scripts (async=false, defer=true) dynamically in an inline script and I test explicitly for MSIE up to 10 in order to show some unsupported browser page and redirecting to a page listing modern browsers. Then I basically test for the presence of window.Promise and window.fetch to determine whether it's a modern browser which will load the legacy src in IE11 and the modern src in the remaining browsers. I don't know yet how to test in older mobile phones... Maybe this technique should be adapted to support relevant old mobile browsers and make them load as IE11 in case they would support Promise and fetch but not ES2015.

This proved to work pretty well in Firefox, Chrome, IE11 (doesn't download both resources) and Edge. I also add link rel=preload as=script in the very beginning of the head section, although the inline script is also in the head section (closer to its end). Didn't notice any difference though by adding link rel=preload. At least not in localhost, haven't test it in a far server yet. rel=prefetch would make IE11 load both bundles, but preload is not supported by IE11, so it's good.

I'm returning an array of configs in webpack and had to include a few tricks to make it work with html-webpack-plugin in order to get both bundles to the template. When I find some time I'll try to publish that configuration somewhere.

@marcobiedermann
Copy link

Firefox 58 and Safari 11 are still downloading both but only execute the legacy script :/

@ralscha
Copy link
Author

ralscha commented Jan 30, 2018

Module support in Firefox 58 is still disabled by default.
You need to enable it in about:config
dom.moduleScripts.enabled

@marcobiedermann
Copy link

@ralscha I did but Firefox is still downloading both files:

screen shot 2018-01-30 at 14 06 44 2

@ralscha
Copy link
Author

ralscha commented Jan 30, 2018

I guess this has something to do with this bug:
https://bugzilla.mozilla.org/show_bug.cgi?id=1382020

Should be fixed in Firefox 60

@VictorKolb
Copy link

VictorKolb commented Apr 24, 2018

@philipwalton
Copy link
Owner

@VictorKolb that solution isn't great as it prevents the browser's preload scanner from detecting the script and initiating the download early.

While definitely hacky, I think it's OK to use document.write() for the legacy script, but I would definitely not use it for the modern script for this reason (i.e. you lose many of the performance gains you get from a smaller script).

I should also point out that the mere presence of document.write() in your code can cause certain slow paths in Chrome's engine.

@pavelloz
Copy link

pavelloz commented Apr 26, 2018

@philipwalton can you point me in the right direction to read about your last point? Im interested in the topic (and ways of mitigating the performance hit).

@webschik
Copy link

@VictorKolb
Copy link

Hi @philipwalton! Big thank for your answer! Unfortunately, if I'll append script via document.appendChild event DOMContentLoaded doesn't triggering. And more: Safari 10 execute both, legacy and modern script. :(
Maybe there are some method to include script to page and don't lose performance?

@rosenfeld
Copy link

I use appendChild of an async script, earlier in the header, plus http2 push (preload header + nginx push support). Couldn't be faster on modern browsers.

@piehei
Copy link

piehei commented Jun 25, 2018

It seems like Safari 11 is broken. It downloads both scripts.

I setup a simple test site over at https://vuetest.surge.sh and ran WPT on it with iOS/Safari 11:
https://www.webpagetest.org/result/180625_WF_b7ad95a50e8d05c01a88fde828f4d3e9/1/details/

@firsttris
Copy link

firsttris commented Jul 4, 2018

a colleague at work found a solution which works for Chrome, Safari 11, IE11, EDGE, FF (only downloads 1 bundle not both)
I wrote down my findings and created a small plugin for html-webpack-plugin for webpack multi build config.. (still in testing)
https://github.com/firsttris/html-webpack-multi-build-plugin

@jakub-g
Copy link
Contributor

jakub-g commented Dec 7, 2018

@firsttris as mentioned earlier in this thread, loading the proper script via JS means that the files are not discovered by the preload scanner of the browser, hence they start being fetched later than they could be by the modern and capable browsers (not great for the majority of the users of up-to-date Chrome, Firefox, Safari).

@jakub-g
Copy link
Contributor

jakub-g commented Dec 7, 2018

To sum up this thread now that the ES modules have been rolled out to all browsers:

The module/nomodule double download problem from the "regular user" pespective:

  • does not exist in Safari when using the trick from https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc according to my tests ✅
  • exists in Firefox 59- (even if modules are disabled by default, they are downloaded); note though that neither Firefox 52 ESR not 60 ESR are affected, hence IMO this is not a big issue. Outdated affected Firefoxes have now a negligible market share ✅
  • exists in Chrome55- (but this is fifteen major Chrome versions back as of late 2018) - not a big issue ✅
  • exists in IE11- ⚠️ (module scripts are fetched, but not executed) It could be solved for IE9- with conditional comments, but IE9- is a tiny fraction of traffic compared to IE11, so probably not worth the trouble.

But here's the bigger problem: Edge downloads the nomodule, and additionally... downloads the module... twice! ¯_(ツ)_/¯
Edge 15-16: two downloads ⚠️ (nomodule and module)
Edge 17-18: three downloads ❌ (nomodule and module x2)

To summarize: IE and Edge are the ones mostly affected, total they have ~5% global market share according to StatCounter (~3% IE, ~2% Edge).

All results in a table: https://gist.github.com/jakub-g/5fc11af85a061ca29cc84892f1059fec

Here's the testpages:

https://jg-testpage.github.io/es-modules/module-nomodule/index.html
https://jg-testpage.github.io/es-modules/module-nomodule/simple.html

@rosenfeld
Copy link

as mentioned earlier in this thread, loading the proper script via JS means that the files are not discovered by the preload scanner of the browser, hence they start being fetched later than they could be by the modern and capable browsers (not great for the majority of the users of up-to-date Chrome, Firefox, Safari)

Downloading a big bundle more than once in some browsers is not something I expect to be okay, so I prefer to use JS for the time being to serve the right bundle for the browser. But all preloading and prefetching is handled by the server-side in the response headers, so I don't think it's a problem to break the preload scanner, but even if that wasn't the case, I still think that it worths using JS to choose the right bundle.

@jakub-g
Copy link
Contributor

jakub-g commented Dec 7, 2018

@rosenfeld as usual it's a tradeoff, depending on what is your exact user demography, what is technically possible given your stack, how critical that will be, and how much effort you want to go to.

Note also my other comment about double-download of module in Edge, which further complicates things... (if you serve module to Edge, there will be two downloads of the same file, so... shall we not serve modules to Edge? that would be backwards)

@rosenfeld
Copy link

Fortunately those quirks with IE and Edge seem to go away soon now that Microsoft is finally giving up on creating a browser engine and it seems they are going to build their browser using the Chromium engine.

It may take an year or so until we can finally stop worrying about all those quirks from Microsoft browsers, but eventually we'll get there.

Until now, I prefer to pay the small price for putting some JS code to choose the right bundle for the browser :) Of course, for those who don't care at all for customers using old browsers, they could simply take the easy path and let them download both bundles ;)

@philipwalton
Copy link
Owner

philipwalton commented Jul 15, 2019

Until now, I prefer to pay the small price for putting some JS code to choose the right bundle for the browser :) Of course, for those who don't care at all for customers using old browsers, they could simply take the easy path and let them download both bundles ;)

@rosenfeld I wouldn't quite characterize this as a "small price".

The cost of shipping lots of unneeded JavaScript to low-end mobile browsers can be significant! We (on the Chrome team) have seen numerous occurrences of polyfill bloat adding seconds to the total startup time of websites on low-end mobile devices. On the other hand, a user on desktop IE or Edge having to download something twice is likely to have zero effect on startup time since the incorrectly downloaded bundle isn't executed and is only downloaded optimistically and off the main thread (by the preload scanner).

In addition to performance cost, there's also a literal monetary cost. Most users affected by the double-download bug are on WiFi with an unlimited data plan. Downloading an extra file doesn't cost them anything—this is often not the case for mobile web users.

All this to say, there are still lots of good reason to adopt this technique now and not wait until it works absolutely perfectly in all browsers.

@rosenfeld
Copy link

rosenfeld commented Jul 15, 2019

I suspect there are good reasons for the Chrome team to recommend that approach, however, I'd suggest people to do their own benchmark if they are really interested in understanding what works best for them.

For our particular application, we don't care about mobile users because the application would only run in desktop anyway. Downloading is a significant part of the initial load time according to our recorded measures (using the Timing API). While lots of users would get a total load time below 1s, there were a few of them that would take 5s or a bit more. In those cases, the download time was the responsible for most of this time. Some clients far from us don't have a good enough download speed from our servers and using a CDN such as Cloudfront didn't improve the situation. Our total bundle size is more than 1MB, so doubling the download time would slow down their first-time load time. That's why we didn't care for paying a very small price with just around 1k or less of extra JavaScript to avoid downloading more code than needed. This approach worked best for us, but I'm sure there are other cases that would benefit from the double download instead of the extra JS, although it's hard for me to believe that such simple and tiny JS to conditionally download the right bundle would add more than a few ms to the total load time.

@philipwalton
Copy link
Owner

I'd suggest people to do their own benchmark if they are really interested in understanding what works best for them.

I definitely agree with this. Everyone should make decisions based on their own uses cases and analytics/metrics, but it's also important that these benchmarks are measuring the right things.

Downloading is a significant part of the initial load time according to our recorded measures (using the Timing API).

I'm pretty sure this is not the case for the double download issue. These browsers are downloading the extra files because their preload scanners are discovering links on the page and optimistically downloading them. But this download is happening asynchronously (off the main thread) and since those files are never executed, they don't block the page load (at least they didn't in my testing).

My point is that just because a file takes 5 seconds to download doesn't mean that two similar files of the same size will take 10 seconds. What thread the file is downloaded on, whether the script is blocking, how many total files are being download, etc. all these things contribute to the total page load time in complex ways that don't necessarily have a linear relationship with the total script download size.

To properly benchmark this, I'd recommend running an A/B test with some fraction of your users trying the module/nomodule approach, and then comparing the p50, p95, and p99 values for you total application load time (whatever metrics you're using) for this test group against your main group.

I suspect that the load time improvements for all your users in your experiment group (which should include desktop Chrome/Firefox/Safari users) will be faster at all of those quantiles than in your main group that currently ships lots of polyfills and unneeded transpilation bloat.

But as you say, it's important to test/measure this for yourself.

@rosenfeld
Copy link

Sure, I wasn't suggesting that those downloads would be blocking the page load in any way. I know they happen in parallel. We send the right HTTP headers for new browsers to discover those links in advance and start downloading them as soon as possible, so let me explain the situation better.

When you're behind some slow Internet connection, the more bytes to download the more it will take to download. Even if you're downloading those two files in parallel, when the connection speed is the bottleneck, having to download just one bundle rather than two is likely to finish that one bundle faster than if there was another one being downloaded in parallel.

I agree that the A/B test setup would be ideal but it also requires much more effort and time, so I'm avoiding using my time on this and I'm focusing on other features instead, but I agree this would be better tested with such setup.

However, it doesn't mean I haven't tried to verify my guesses somehow. The way I did was by limiting the bandwidth using some tools and it indeed loaded the application faster with the tiny JS help rather than letting the browser download the two bundles in IE11.

@philipwalton
Copy link
Owner

I'm glad you're testing this and basing your decisions on data rather than just feeling. I still do think that the speed improvements you'd see on modern browsers (used by the vast majority of users) would, in sum, outweigh any speed regressions you see on older browsers (used by a minority of users), but I do understand if there are specific business reasons that prevent you from making the switch.

Also, keep in mind that what I'm saying here is not meant to convince you (in particular) to adopt the module/nomodule pattern. I'm writing all this here so other folks reading this thread can better understand the actual pros and cons of a technique like this, as well as its effect on real users.

@rosenfeld
Copy link

Yes, I do understand that. Just out of curiosity, there are still some people using our application through IE 11, that's why we still support it. Having said that, I didn't notice any slow down in modern browsers by the introduction of this tiny script. If I disable the inline script and simply serve a single modern script and test in modern browsers I see absolutely no difference in the total load time with or without cache, but indeed, I haven't measured the impact of such scripts in mobile browsers, so maybe there would be some difference there.

@philipwalton
Copy link
Owner

@rosenfeld are you referring to this script? If so, that's only needed for Safari 10.1 and iOS 10.3, which are pretty old at this point and some sites may be able to drop that script entirely.

@rosenfeld
Copy link

No, it's a custom script but I have already left for today. I'll share it here tomorrow morning.

@rosenfeld
Copy link

Oh, I was almost forgetting about this thread, sorry. Here's the script I embed inline in the application I maintain:

<script id="boot-script" nonce="891b6a0d35ed2b5b810db9b7a69fd0df">
        var scripts = [["/assets/runtime~main.ffef36dc70dbe0b0bb00.js", "/assets/runtime~main-legacy.legacy.c8afd5c74b9846e6d6c3.js"], ["/assets/main.edb956b662fcd9c6f4a3.js", "/assets/main-legacy.legacy.6b2f451938b205684c08.js"]];
        var modernBrowser = window.Promise && window.fetch;
        scripts.forEach(function(srcs){
          var script = document.createElement('script');
          script.crossOrigin = 'anonymous';
          script.async = false; script.defer = true;
          script.src = modernBrowser ? srcs[0] : srcs[1];
          document.head.appendChild(script);
        });
        var thisScript = document.getElementById('boot-script');
        thisScript.parentNode.removeChild(thisScript);
</script>

The script paths are injected by the server-side code.

@philipwalton
Copy link
Owner

As I mention in #1 (comment), imperative solutions like this do prevent the browser's preload scanner from detecting the scripts, so if you use this you'd probably also want to use it with <link rel="preload"> (or modulepreload if you were loading it as a module).

BTW, my colleague Jason recently wrote a post on some of the options for avoiding the double-download when using module/nomodule. If you haven't see it, I'd check it out: https://jasonformat.com/modern-script-loading/

@rosenfeld
Copy link

Yes, I know, we send the link in the HTTP headers for modern browsers to preload them.

@rosenfeld
Copy link

I'll take a look at this link later, thanks!

@rosenfeld
Copy link

I've just read that article you linked, yes, that's the technique we use, sorta, but another option, instead of using the link tag to preload the script is to use the preload HTTP header. This is what we have adopted for our application instead of the link tag.

@AndrewGibson27
Copy link

Webkit has patched the Safari 11+ double-fetch bug: https://bugs.webkit.org/show_bug.cgi?id=194337

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests