Using Webpack to Build Cross-Browser Compatible Apps
Configure Webpack, Babel, and polyfills around one browser support policy. Then verify the emitted runtime, application code, and APIs in the browsers you support.
To build cross-browser compatible apps with Webpack, define the browsers you support in Browserslist, use that same policy for Webpack’s runtime target and Babel’s source transformations, add only the API polyfills those browsers need, and test the emitted bundles in the browsers you promise to support. Webpack’s target affects Webpack-generated runtime code; it does not transpile JavaScript you wrote. Babel transforms application syntax, and polyfills supply missing APIs. Those are separate compatibility jobs.
This guide uses Webpack 5, Babel, and npm. Check the versions and existing configuration in your project before copying it, since option support can vary by version.
1. Declare the browser support policy
Start by deciding which browser versions your product must support. Put that policy in a Browserslist configuration so Webpack and Babel can use the same source of truth. The examples below use a broad modern-browser policy; replace it with the exact browsers and versions your users require.
// package.json
{
"scripts": {
"build": "webpack --mode production"
},
"browserslist": [
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 Edge versions"
]
}
Webpack can use the nearest package configuration or the BROWSERSLIST environment variable when its target is browserslist. You can also define named Browserslist environments and select one explicitly; ensure the target and Babel resolve the intended environment consistently. See the official Webpack target documentation.
Do not use a generic modern-browser list if you have a contractual or user-driven legacy requirement. For example, if you must support IE 11, add it to the support policy and account for its syntax and API gaps. Webpack’s v4-to-v5 migration guide also documents target: ['web', 'es5'] as an IE 11-oriented target option.
2. Install the build dependencies
For the Babel setup below, install Webpack, its CLI, Babel’s loader and preset, and core-js for optional usage-based polyfills:
npm install --save-dev webpack webpack-cli babel-loader @babel/core @babel/preset-env
npm install core-js
If your project already has a package manager lockfile or uses a different package manager, keep its normal install workflow and align Babel, Webpack, and loader versions with your project.
3. Configure Webpack’s runtime target and Babel’s source transform
Create a Webpack configuration that uses Browserslist for Webpack’s runtime and runs your application modules through Babel. The include path limits Babel to your own source directory; dependencies may need separate treatment if one contains syntax your oldest browser cannot parse.
// webpack.config.js
const path = require('node:path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'app.js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
target: 'browserslist',
module: {
rules: [
{
test: /\\.m?js$/,
include: path.resolve(__dirname, 'src'),
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}]
]
}
}
}
]
}
};
target: 'browserslist' selects runtime assumptions based on the configured browser matrix. @babel/preset-env uses Browserslist to transform unsupported syntax in your source. These settings complement each other; a Webpack target does not replace Babel. Webpack documents that it does not transpile authored code just because a target is configured. See Webpack output configuration for runtime output controls and the Webpack shimming guide for the Babel and Browserslist approach.
With useBuiltIns: 'usage', Babel can add imports for core-js features used in transformed files according to the target matrix. The corejs: 3 setting should match the installed core-js major version; use a version form supported by the Babel preset-env version installed in your project. If you prefer to manage polyfills manually, remove these options and import only the polyfills you need.
4. Add API polyfills in the right order
Syntax transformation and API polyfilling solve different problems. Babel may rewrite syntax so an older browser can parse it, while that browser still lacks a runtime API your code calls. Determine API requirements from your app and its dependencies, then include suitable polyfills before dependent code executes.
Webpack specifically notes that import() and require.ensure() need Promise; older browsers may require a Promise polyfill. If using manual imports, put the polyfill first in the entry array:
// webpack.config.js: manual Promise polyfill entry ordering
module.exports = {
entry: [
'core-js/features/promise',
'./src/index.js'
]
};
Use either a deliberate manual polyfill strategy or Babel’s usage-based injection with an appropriate Browserslist configuration. Avoid importing every stable polyfill without a reason. Webpack’s entry documentation gives a version-specific example: importing all of core-js/stable with core-js 3.50 pulled 637 modules, 215 KB minified and 71 KB gzipped in that example. These figures illustrate the possible cost of a full import; they are not a prediction for every application. See Webpack entry and context.
5. Check dependencies and Webpack 5 Node module behavior
Transpiling only src is a sensible starting point, but it does not guarantee every dependency is compatible with your oldest browser. If a dependency publishes syntax your browser cannot parse, determine whether it offers a compatible build and configure Babel to process that dependency selectively. Avoid transpiling every package indiscriminately: it can increase build time and complicate dependency behavior.
Webpack 5 also no longer automatically supplies browser polyfills for Node.js core modules. If browser code imports a Node core module, first check whether the dependency has a browser-oriented entry or whether the import is accidental. Add an explicit browser-compatible implementation only when the feature is needed and the dependency supports that configuration. See Webpack resolve configuration.
6. Build and inspect the output
Run the production build:
npm run build
A successful build confirms that Webpack could produce output. It does not prove that every supported browser can parse and run the application. Validate the actual JavaScript files in dist, including Webpack’s runtime and any lazy-loaded chunks, against your declared browser policy. Test the APIs your app uses as well as initial load and routes that trigger dynamic imports.
7. Decide whether to ship one bundle or modern and legacy bundles
A single bundle is simpler to configure, serve, cache, and test. Separate modern and legacy builds may reduce the JavaScript and polyfills downloaded by newer browsers, but they add build configuration, browser selection in the page, test cases, and cache considerations. Choose dual builds only after comparing that maintenance work with the actual browser mix and download needs of your application. Webpack’s shimming guide demonstrates the dual-build approach; the tradeoff depends on your project.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Older browser reports a syntax error in the bundle | The source or a dependency was not transformed, or the Webpack runtime target is too modern. | Confirm the browser is in Browserslist, Babel processes the offending module, and Webpack uses the intended target. Inspect the emitted file and check dependencies separately. |
Promise is undefined during dynamic import or another async feature |
The browser lacks Promise, and no polyfill ran before the feature was used. | Add a Promise polyfill before dependent code or configure usage-based polyfill inclusion for the browser matrix. |
| Build succeeds, but a browser fails at runtime | Compilation does not guarantee API availability or correct runtime behavior. | Reproduce in the oldest supported browser. Check missing APIs, dynamic chunk loading, dependency code, and polyfill order. |
Can't resolve 'path' or another Node core module |
Browser-targeted code or a dependency expects a Node module; Webpack 5 no longer injects Node core polyfills automatically. | Look for a browser-compatible dependency entry or remove the Node-only import. Add an explicit supported browser implementation only if needed. |
| Polyfill bundle is unexpectedly large | A broad polyfill import includes features the supported browsers or app do not need. | Use usage-based inclusion with Browserslist or import a narrow set of required polyfills, then inspect production output. |
| Babel does not transform a failing package | The loader rule excludes dependencies or includes only the app source directory. | Extend the rule for that specific package, or choose a compatible package build. Keep the exception narrow and recheck build time. |
Performance, reliability, and cost considerations
- Bundle size: Transform syntax and include polyfills based on the actual support policy. Full polyfill imports can add substantial weight; usage-based inclusion avoids many unused features.
- Build time: Transpiling all dependencies can slow builds. Start with application source and expand only for packages that require it.
- Reliability: Keep Webpack and Babel aligned to one browser matrix, pin dependency versions through your normal lockfile process, and test runtime behavior in the oldest supported browser versions.
- Operations: A legacy target can constrain syntax and runtime choices. Revisit the matrix when product requirements change, and remove obsolete polyfills only after support policy and usage justify it.
- Cost: Polyfills and dual builds trade download size against implementation and maintenance effort. Measure your own production assets and test burden; the documentation’s core-js example is not a project-specific estimate.
Or skip the browser setup
If your task is to capture rendered pages across browser conditions, ScreenshotNeo provides a screenshot API and MCP server. One GET request returns an image or PDF; the browser compatibility build guidance above still applies to your own application.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
See the ScreenshotNeo API documentation for request options and response details. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up for ScreenshotNeo and get 1,000 screenshots a month free, with no card.
ScreenshotNeo also supports full-page and element capture, device presets, custom CSS and JavaScript, wait conditions, request blocking, PDFs, caching, signed links, asynchronous jobs, bulk capture, and a usage API. See ScreenshotNeo for product details.
FAQ
Does Webpack transpile my JavaScript when I set target?
No. The target controls Webpack-generated runtime assumptions and output features. Use Babel or another source transpiler for your authored JavaScript.
Does Babel add every missing browser API?
No. Syntax transformation does not automatically supply every runtime API. Identify the APIs your app uses and include the required polyfills in the correct order.
Can I support IE 11?
It requires an explicit policy for runtime syntax and APIs. Webpack’s migration guidance recommends including IE 11 in Browserslist or using a web/ES5 target combination, alongside source transpilation and needed polyfills.
Does a successful build prove cross-browser compatibility?
No. Test the emitted runtime and application, including lazy-loaded chunks and API behavior, in the browser versions your product promises to support.


