Rsbuild supports modern browsers by default and provides syntax and API downgrade capabilities to ensure compatibility with legacy browsers that support ES5 (such as IE11).
This chapter explains how to use Rsbuild's features to address browser compatibility issues.
Before dealing with compatibility issues, you need to determine which browsers your project needs to support and add the corresponding browserslist config.
If you haven't set browserslist yet, please read the Browserslist chapter first.
If you have set a browserslist, Rsbuild automatically compiles to match that scope, downgrades JavaScript and CSS syntax, and injects required polyfills. In most cases, you can safely use modern ECMAScript features without worrying about compatibility issues.
After setting the browserslist, if you still encounter compatibility issues, continue reading to find solutions.
A polyfill is code that provides the functionality of newer features to older browsers that don't support them natively. It fills gaps in older browsers' implementations of web standards, allowing developers to use newer features without worrying about whether they'll work in older browsers. For example, if a browser doesn't support the Array.prototype.flat() method, a polyfill can provide that functionality, allowing code that uses Array.prototype.flat() to work in that browser. Polyfills are commonly used to ensure web applications work across a wide range of browsers, including older ones.
Before dealing with compatibility issues, it is recommended that you understand the following background knowledge to better handle related issues.
When you use higher-version syntax and APIs in your project, to make the compiled code run reliably in lower-version browsers, we need to downgrade two parts: syntax and APIs.
Rsbuild downgrades syntax through transpilation and downgrades APIs through polyfill injection.
Syntax and APIs are not tightly coupled. When browser manufacturers implement the engine, they will support some syntax or implement some APIs in advance according to the specification or their own needs. Therefore, browsers from different manufacturers in the same period are not necessarily compatible with syntax and API. So in general practice, syntax and API are handled in two parts.
Syntax is a set of rules for how a programming language organizes code, and code that doesn't follow these rules cannot be correctly recognized by the programming language's engine and therefore cannot run. In JavaScript, the following are examples of syntax rules:
const foo = 1, const means to declare an immutable constant.foo?.bar?.baz, ?. indicates optional chaining of access properties.async function () {}, async means to declare an asynchronous function.Because different browser parsers support different syntax, especially older browser engines support less syntax, some syntax will cause errors when run in older browser engines at the AST parsing stage.
For example, the following code will cause an error in IE or an older version of Node.js:
When we run this code in a low version of Node.js, the following error message will appear:
It's obvious from the error message that this is a syntax error, meaning this syntax is not supported in older versions of the engine.
Syntax cannot be supported by polyfill or shim. To run syntax that's not originally supported in a lower-version browser, you need to transpile the code into syntax the lower-version engine can support.
Transpile the above code into the following to run in older version engines:
After transpilation, the syntax of the code has changed, and some syntax the lower-version engine cannot understand has been replaced with syntax it can understand, but the meaning of the code itself hasn't changed.
If the engine encounters an unrecognized syntax when converting to AST, it will report a syntax error and abort code execution. In this case, if your project doesn't use capabilities such as SSR or SSG, the page will be blank, making it unavailable.
If the code is successfully converted to AST, the engine will convert the AST into executable code and run it normally inside the engine.
JavaScript is an interpreted scripting language, unlike compiled languages like Rust. Rust checks calls in the code during compilation, but JavaScript doesn't know whether a function called by a line of code exists until it actually runs to that line, so some errors only appear at runtime.
For example:
The above code has correct syntax and can be converted to AST correctly in the first stage of the engine runtime, but when it is actually running, because the method notExistedMethod does not exist on String.prototype, an error will be reported:
With the iteration of ECMAScript, new methods are added to built-in objects. For example, String.prototype.replaceAll was introduced in ES2021. The replaceAll method doesn't exist in the built-in object String.prototype of most browser engines before 2021, so the following code works in the latest Chrome but not in earlier versions:
To solve the problem that String.prototype lacks replaceAll in older browsers, we can extend the String.prototype object in older browsers and add the replaceAll method to it. For example:
This technique of providing implementations for legacy environments to align new APIs is called polyfill.
By default, Rsbuild uses SWC to compile all JavaScript and TypeScript modules, excluding JavaScript modules in the node_modules directory.
This approach is designed to avoid impacting build performance when downgrading all third-party dependencies while also preventing potential issues from redundantly downgrading pre-compiled third-party dependencies.
The source code of the current project will be downgraded by default, so you don't need to add additional config, just make sure that the browserslist config is set correctly.
When you find that a third-party dependency causes compatibility issues, you can add this dependency to Rsbuild's source.include config. This makes Rsbuild perform extra compilation for that dependency.
Taking the npm package query-string as an example, you can add the following config:
See source.include for detailed usage.
Rsbuild compiles JavaScript code using SWC and supports injecting polyfills such as core-js and @swc/helpers.
In different usage scenarios, you may need different polyfill solutions. Rsbuild provides output.polyfill config to switch between different polyfill modes.
Rsbuild does not inject any polyfills by default:
When you enable usage mode, Rsbuild will analyze the source code in the project and determine which polyfills need to be injected.
For example, the code uses the Map object:
After compilation, only the polyfills for Map will be injected into this file:
The advantage of this method is smaller injected polyfill size, which is suitable for projects with higher requirements on bundle size. The disadvantage is that polyfills may not be fully injected because third-party dependencies won't be compiled and downgraded by default, so the polyfills required by third-party dependencies won't be analyzed. If you need to analyze a third-party dependency, you also need to add it to source.include config.
The config of usage mode is:
When using entry mode, Rsbuild will analyze which core-js methods need to be injected according to the browserslist set for the current project and inject them into the entry file of each page. Polyfills injected this way are more comprehensive, eliminating concerns about polyfill issues in project source code and third-party dependencies. However, because some unused polyfill code is included, the bundle size may increase.
The config of entry mode is:
Cloudflare provides a polyfill service that automatically generates polyfill bundles based on the user's browser User-Agent.
You can use the html.tags config of Rsbuild to inject scripts. For example, to inject a <script> tag at the beginning of the <head> tag:
It should be noted that core-js cannot provide polyfills for all JavaScript APIs. Some APIs cannot be fully simulated through polyfills due to the complexity of their underlying implementation or performance considerations.
The most typical example is the Proxy object. Since Proxy requires engine-level support to implement object operation interception, its behavior cannot be fully simulated through pure JavaScript code, so core-js doesn't provide a polyfill for Proxy.
See core-js - Missing polyfills to understand which APIs core-js cannot provide polyfills for.