- This is a translated version of the original article: https://techblog.woowahan.com/17710/
- Baemin is a popular food delivery app in Korea
Need for older version browser support
Baemin app's shopping cart is implemented with a webview, which in turn is developed with React and Vite. As it is a screen with a lot of users, there are various tasks for us, the frontend developers.
Recently, older versions of Safari and Chrome browsers have experienced compatibility issues: not allowing users enter specific pages. If you use some code syntax or APIs that are not supported by older browser versions, users are exposed to only white screens.
In this situation, updating the browser or the OS would be the best solution, but it would be more user-friendly if the service was available without the update. In particular, since nearly 10,000 users of Baemin app are using older versions (iOS 12 and Android 7), if you give up supporting older versions, there could be a significant business impact on sales. Therefore, it can be said that supporting older versions is also important from a business perspective.
In this article, let's look at how Vite's official plug-in, @vitejs/plugin-legacy
, supports older versions of the browser, and how we've solved the problems we encountered along the way.
Support for older versions of browsers utilizing the Vite Legacy Plug-in
Troubleshooting old browser grammatical errors
We recently shifted webpack
(Created-React-App) to ESBuild
+Rollup(Vite)
. This process led to some issues, and the most critical one was when the user entered the screen that rendered blank content.
The first time I noticed the problem was from the error mail that I received from Sentry. It said: Object.fromEntries is not a function
.
When I checked the OS that was causing the problem,** it was occurring in browsers below Chrome 73*. The shopping cart of Baemin app is implemented in **WebView, which renders the web application based on the browser version installed on the user's device*. Therefore, for users using older versions of Android or iOS, the shopping cart is opened in a lower version of WebView that does not support the latest browser functionality. This creates an issue for browsers below Chrome 73.
In fact, I downloaded and tested the old version of Chromium through the old version Chromium Snapshot repository and found that there was a syntax error in the devTools.
Here's how the tests were conducted:
- Download the file from the older version of Chrome snapshot repository to mac 65.0.3325.146 download_url
- Run
vite build && vite preview
- Connecting to the vite preview development server from Chromium (e.g.,
localhost:4173/cart
) - Open the developer tool and run
Object.fromEntries({})
- Check errors, if any
We already had applied @vitejs/plugin-legacy
to support the older versions, but the error was still occurring. Below are the settings at the time of the error.
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
legacy({
targets: ['chrome >= 64', 'safari >= 12']
})
]
})
At that time, the minimum supported version was set to Chrome 64, Safari 12 or higher, which are the minimum versions required to use import.meta
, according to Can I Use.
I thought that setting targets would provide polyfill support right away, but I realized that I was mistaken while carefully reading the docs.
According to the official docs, setting targets is not enough, but some additional setting is needed in the polyfills
or modernPolyfills
field.
What is Polyfill?
Polyfill is code that allows you to use features that are not supported by certain browsers. Because different browsers have different JavaScript features, using the latest grammar can cause errors in older browsers. To resolve this issue, use Polyfill.
In conclusion, the error Object.fromEntries is not a function
could be solved by adding the modernPolyfills
option as follows.
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
legacy({
targets: ['chrome >= 64', 'safari >= 12'],
modernPolyfills: ['es.object.from-entries'],
})
]
})
Let me explain step by step how this code was able to solve the problem.
Understanding the 'Modern' and 'Legacy' concepts of legacy plug-ins
First, we need to understand the concepts of "modern" and "legacy" used by the legacy plug-ins. Just remember the terms below, and you'll find it much easier to understand what we're going to talk about.
- Modern: Environments that support Native ESM (Chrome >= 64, Safari >= 12)
- Legacy: Environments that do not support Native ESM (Chrome <64, Safari <12)
Native ESM (ECMAScript modules) means supporting ESM directly in a browser or Node.js environment without any additional bundling process.
ESM is the official module system of JavaScript that allows you to define and invoke modules using import/export syntax.
Older browsers don't support Native ESM, so you need to use a transpiler such as Babel or convert it to an earlier version of JavaScript through a bundler such as Webpack.
The concepts of "legacy" and "modern" used by Vite are divided based on whether Native ESM is supported or not. In other words, the browsers that support Native ESM process modern chunks, while the rest process legacy chunks.
If you've ever applied the legacy plug-in to the Vite configuration file, the result built through the Vite build command is a legacy chunk and a modern chunk (yes, both). Then, a script tag running JavaScript determining whether to use a legacy chunk or a modern chunk is inserted into the HTML file.
How modern and legacy browsers process the chunks
Let's take a look at the code that for branching in modern and legacy browsers. This is good to know, but it's no problem if you just know the conclusions, so you can just scroll down if you want.
Below is code that separates Modern and Legacy chunks from actual built HTML files.
<head>
<script type="module" crossorigin="" src="/assets/index-e45b7e40.js"></script>
<script type="module">
import.meta.url;
import("_").catch(() => 1);
async function* g() {}
if (location.protocol != "file:") {
window.__vite_is_modern_browser = true;
}
</script>
<script type="module">
!(function () {
if (window.__vite_is_modern_browser) return;
console.warn(
"vite: loading legacy chunks, syntax error above and the same error below should be ignored",
);
var e = document.getElementById("vite-legacy-polyfill"),
n = document.createElement("script");
(n.src = e.src),
(n.onload = function () {
System.import(
document
.getElementById("vite-legacy-entry")
.getAttribute("data-src"),
);
}),
document.body.appendChild(n);
})();
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script nomodule="">
!(function () {
var e = document,
t = e.createElement("script");
if (!("noModule" in t) && "onbeforeload" in t) {
var n = !1;
e.addEventListener(
"beforeload",
function (e) {
if (e.target === t) n = !0;
else if (!e.target.hasAttribute("nomodule") || !n) return;
e.preventDefault();
},
!0,
),
(t.type = "module"),
(t.src = "."),
e.head.appendChild(t),
t.remove();
}
})();
</script>
<script
nomodule=""
crossorigin=""
id="vite-legacy-entry"
data-src="/assets/index-legacy-38fcc150.js"
>
System.import(
document.getElementById("vite-legacy-entry").getAttribute("data-src"),
);
</script>
</body>
In summary, browsers that support Native ESM use the script in the head tag to call modern chunks, and browsers that do not support Native ESM use the script in the body tag to call legacy chunks.
Let's take a closer look at the scripts. First, for modern browsers that support Native ESM, call the module through the script defined in the head tag. The script below is the first to run between all scripts.
<script type="module" crossorigin="" src="/assets/index-e45b7e40.js"></script>
However, there is a flaw in this script. There's a range of versions that support Native ESM but do not use any core syntax, such as import.meta
. The Native ESM supported version (available version of type="module"
) is Chrome >= 61, Safari >= 11, but the versions that support all of the import.meta
, dynamic import
, and async generator
functions are Chrome >= 64, Safari >= 12.
As a result, an error may occur when calling ESM from a version that does not fully support ESM. The legacy plug-in adds the following two additional scripts in consideration of this situation.
<script type="module">
import.meta.url;
import("_").catch(() => 1);
async function* g() {}
if (location.protocol != "file:") {
window.__vite_is_modern_browser = true;
}
</script>
<script type="module">
!(function () {
if (window.__vite_is_modern_browser) return;
console.warn(
"vite: loading legacy chunks, syntax error above and the same error below should be ignored",
);
var e = document.getElementById("vite-legacy-polyfill"),
n = document.createElement("script");
(n.src = e.src),
(n.onload = function () {
System.import(
document.getElementById("vite-legacy-entry").getAttribute("data-src"),
);
}),
document.body.appendChild(n);
})();
</script>
For browsers that do not support all modern syntaxes such as import.meta
, dynamic import and async generator functions, the window._vite_is_modern_browser
variable is set to false, and then the legacy chunk and legacy polyfill are imported.
Legacy browsers that do not support Native ESM go through a slightly different process: the first script tag is ignored because script type "module"
is not supported, and the window._vite_is_modern_browser
variable is not set to true in the second script tag. Therefore, the first script of the script tag is executed to import legacy polyfill and legacy chunks.
<script nomodule="">
!(function () {
var e = document,
t = e.createElement("script");
if (!("noModule" in t) && "onbeforeload" in t) {
var n = !1;
e.addEventListener(
"beforeload",
function (e) {
if (e.target === t) n = !0;
else if (!e.target.hasAttribute("nomodule") || !n) return;
e.preventDefault();
},
!0,
),
(t.type = "module"),
(t.src = "."),
e.head.appendChild(t),
t.remove();
}
})();
</script>
Above code uses a little trick to check for noModule
and onbeforeload
events to confirm that they are legacy browsers, and then the script tag uses System.import
to call a legacy chunk (/assets/index-legacy-38fcc150.js
).
<script
nomodule=""
crossorigin=""
id="vite-legacy-entry"
data-src="/assets/index-legacy-38fcc150.js"
>
System.import(
document.getElementById("vite-legacy-entry").getAttribute("data-src"),
);
</script>
In this way, Vite's legacy plug-in creates scripts to selectively import modern and legacy chunks, depending on whether the browser supports functionality. Modern browsers do not run unnecessary legacy-related codes, so you can run the latest code without compromising performance. In legacy browsers, on the other hand, you can ensure browser compatibility by importing and executing legacy chunks along with polypill.
Fixing problems with modernPolyfills
options
Now, let's talk about the modernPolyfills
options that we added to vite configuration for troubleshooting.
modernPolyfills
are the options that specify the polyfills to include in the modern chunk. You can specify the polyfills for the syntax you want, or set auto-detection to retrieve all the required syntax. In the latter case, the final bundle capacity can increase from 15KB to 40KB.
The aforementioned Object.fromEntries
error is a problem with Chrome >= 64 or Safari >= 12 versions, which is classified as a modern browser. Therefore, we decided to put es.object.from-entries
polyfill in the modern Polyfills option to include polyfill in the modern chunk:
modernPolyfills: ['es.object.from-entries'],
With this option, we could fix Object.fromEntries is not a function
error.
However, I discussed with my teammates before deployment and we agreed that setting modernPolyfills
to true is better choice in terms of maintenance, even if the bundle size increases slightly.
This is because we agreed that it would be more efficient in the long run to include all the polyfills needed at once rather than responding to individual issues.
Of course, there were concerns about performance due to increased bundle size, but we considered that there are still a significant number of users using legacy browsers, and that abandoning support could lead to potential revenue loss.
In addition, increasing customer inquiries would lead to more workload on the customer support team. We determined that it would be a better option in the long run to ensure stability and maintenance to provide the same experience for all users and minimize customer inquiries.
In addition, we expected that the impact on the user experience would be limited due to the small increase in bundle size.
Therefore, we finally changed the modernPolyfills
option to true. The Vite configuration file that was finally deployed is as follows.
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
legacy({
targets: ['chrome >= 64', 'safari >= 12'],
modernPolyfills: true
})
]
})
But one question may arise here. Modern chunks have been set to include polyfills, so what happens to legacy chunks?
polyfills
Options and the Legacy Chunks
The polyfill for legacy chunks can be specified using the polyfills
option, which is true by default. If the polyfills option is true, Vite automatically includes the ES syntax polyfill in the legacy bundle by analyzing the browser range and project code specified in the targets. Therefore, there is no operating issue due to missing polyfill in legacy browsers.
In short, you can create bundles that work reliably with both modern and legacy browsers by taking advantage of the modern and polyfills options.
ResizeObserver
bug
Just when I thought all issues were fixed after completing the deployment, another issue occurred.
I received an email from Sentry indicating
the following error: Can't find variable: ResizeObserver
in Safari 12 browser. It turns out, ResizeObserver
is supported with iOS Safari 13.4 and later, Chrome 64 and later.
I expected the legacy plug-in to automatically include the required polyfill, but it was actually a problem with the missing polyfill for ResizeObserver
.
Setting modernPolyfills
to true loads Babel's autosensing polyfill loading script, which contains only ES syntax-related polyfills and does not include a separate web API such as ResizeObserver
, which is also specified in the Vite Legacy Plugin docs:
additionalLegacyPolyfills
Type:string[]
Add custom imports to the legacy polyfills chunk. Since the usage-based polyfill detection only covers ES language features, it may be necessary to manually specify additional DOM API polyfills using this option.
Therefore, we needed to add a polyfill for ResizeObserver
separately. And both 'legacy' and 'modern' chunks should contain this polyfill. This is because it is classified as a 'modern' browser from Safari 12, but ResizeObserver
is supported from Safari 13.
On the other hand, while I was looking at the docs, I noticed that if polyfill was needed in both modern and legacy browsers, the application code should define it directly and use it.
This allows you to resolve the issue by importing npm packages such as @juggle/resize-observer from the entry file, or by adding the polyfill.io script tag directly to index.html.
I initially tried to use polyfill.io , but I decided to use the @juggle/resize-observer library for several reasons.
First of all, there is a possibility of failure if you use external services. Failing to load scripts may prevent polyfill from functioning in older versions of browsers, and service delays may cause initial load speeds to slow down regardless of the version. There have been a few instances of polyfill.io service down, in fact. There have also been issues related to ownership changes for polyfill.io . (It now appears to have been moved to Cloudflare and stabilized.)
On the other hand, direct use of the https://github.com/juggle/resize-observer library used by polyfill.io can reduce dependencies on external services to ensure stability. The library has been maintained steadily until 2022, and it has been proven to be stable by many users, so we decided to introduce it.
Finally, we added the following code to the Vite entry point (main.tsx).
import { ResizeObserver } from '@juggle/resize-observer';
window.ResizeObserver = window.ResizeObserver || resizeObserverPolyfill;
Later, I was able to run the server with the built version with the vite build && vite preview command and see that ResizeObserver was polyfilled in the Chrome 62 browser.
Limitations of legacy plug-ins and browser compatibility concerns
So far, we've shared the issues I've experienced while trying to support older versions of the browser through the Vite Legacy Plug-in, as well as the resolution process.
While it is ideal to provide the same experience for every web service users, in reality, it is not easy to support all different browsers and versions.
In this context, Vite's legacy plug-in is a great aid for developers. It creates bundles for modern and legacy browsers, automatically branches them through scripts, and easily sets up polyfills for ES syntax, greatly reducing the burden on developers. This allows you to maintain a modern development environment without having to worry too much about complex browser compatibility issues.
But legacy plug-ins aren't all-around. In addition to ES syntax, additional polyfill is sometimes required, like the ResizeObserver
example. You need to be able to accurately recognize the limitations of plug-ins.
Additionally, as we offer bundles for modern browsers and bundles for legacy browsers at the same time, we need to think about the disadvantages of increasing overall bundle size and build time. Depending on the situation, you need to decide whether to selectively apply polyfill or secure stability even if the capacity increases.
Even after the end of Internet Explorer, front-end developers are still suffering from browser compatibility. Nevertheless, I hope tools like Vite's legacy plug-in, which we've introduced in this article, can help ease your burden a little.
Top comments (0)