browserify, jspm, parcel, requirejs, rollup, systemjs, vite, and webpack are tools that help manage JavaScript modules in web applications. They address how code is organized, loaded, and optimized for browsers. Some bundle all code into a single file (like webpack or rollup), others load modules dynamically at runtime (like systemjs or requirejs), and newer tools like vite leverage native ES modules for faster development. These tools differ in configuration complexity, build speed, output format support, and suitability for different project types — from legacy browser support to modern development workflows.
Choosing the right tool to manage JavaScript modules can make or break your frontend architecture. The landscape includes bundlers that combine code (webpack, rollup), loaders that fetch modules at runtime (systemjs, requirejs), and modern dev servers that skip bundling entirely (vite). Let’s cut through the noise with real code and concrete trade-offs.
Some tools bundle everything into one or few files for production. Others load modules dynamically in the browser. This fundamental difference shapes your entire workflow.
These tools process your code ahead of time. You write modular code, and the tool outputs optimized bundles.
// src/index.js
import { greet } from './utils.js';
document.body.innerHTML = greet('World');
All these tools consume this and produce a single script tag in your HTML.
These tools let the browser load modules on demand using a loader script.
<!-- With SystemJS -->
<script src="systemjs/dist/system.js"></script>
<script>
System.import('./main.js');
</script>
Your main.js uses standard or legacy module syntax, and the loader handles fetching and execution.
Vite serves source files directly using native ES modules in development, then bundles with Rollup for production.
<!-- Vite's dev server serves this as-is -->
<script type="module" src="/src/main.js"></script>
No bundling during development means instant startup.
How much setup do you need to get started?
Parcel works out of the box:
npx parcel index.html
Just point it at an HTML file, and it handles JS, CSS, images, etc.
Vite is similarly simple for modern projects:
// vite.config.js (optional)
export default {
root: 'src'
}
Run vite and you’re live.
Rollup needs a config for anything beyond basic JS:
// rollup.config.js
export default {
input: 'src/main.js',
output: { file: 'dist/bundle.js', format: 'es' },
plugins: [/* ... */]
};
Browserify uses command-line transforms:
browserify src/index.js -t babelify -o dist/bundle.js
Webpack requires detailed configuration even for basic setups:
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') },
module: {
rules: [
{ test: /\.js$/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }
]
}
};
These require bootstrapping scripts but no build step:
// systemjs.config.js
System.config({
map: { 'lodash': 'https://cdn.skypack.dev/lodash' }
});
// requirejs config
require.config({
paths: { 'jquery': 'https://code.jquery.com/jquery-3.6.0.min' }
});
JSPM uses import maps:
<script type="importmap">
{ "imports": { "lodash": "https://jspm.dev/lodash" } }
</script>
Hot module replacement (HMR), startup time, and error messages matter daily.
Vite starts in milliseconds because it doesn’t bundle during development:
// Changes reflect instantly without full rebuild
import { createApp } from 'vue'
createApp(App).mount('#app')
Webpack’s HMR works well but requires config:
// webpack.config.js
module.exports = {
devServer: { hot: true }
};
Parcel has HMR built-in with zero config.
Rollup has no dev server by default (use rollup-plugin-serve).
Browserify, RequireJS, SystemJS, and JSPM have no built-in HMR — you’ll refresh manually or add third-party tools.
How well does the tool minify, split, and tree-shake your code?
Rollup was built for tree-shaking:
// utils.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
// main.js
import { add } from './utils.js';
console.log(add(2, 3));
// multiply is excluded from output
Webpack also tree-shakes effectively with proper config.
Parcel and Vite (via Rollup in prod) do good tree-shaking by default.
These don’t optimize output. Browserify bundles everything you require(). The others load modules as-is, so you must optimize upstream.
Need to support IE11 or old mobile browsers?
All can transpile and polyfill for old browsers with Babel and core-js.
Rollup can target older browsers but requires manual setup.
Vite does not support IE11 and targets modern browsers only.
SystemJS can load polyfills for older browsers.
JSPM’s CDN serves modern code by default; legacy support needs extra work.
RequireJS predates modern module standards and isn’t designed for today’s tooling.
Let’s see how each handles a simple app with CSS and JSX.
// src/app.jsx
import './app.css';
export default () => <h1>Hello Vite!</h1>;
/* src/app.css */
h1 { color: blue; }
Needs loaders for JSX and CSS:
// webpack.config.js
module.exports = {
module: {
rules: [
{ test: /\.jsx$/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }
]
}
};
Requires plugins:
// rollup.config.js
import babel from '@rollup/plugin-babel';
import css from 'rollup-plugin-css-only';
export default {
plugins: [babel({ extensions: ['.jsx'] }), css()]
};
Zero config — just run:
parcel src/app.jsx
Install @vitejs/plugin-react, then:
// vite.config.js
import react from '@vitejs/plugin-react';
export default { plugins: [react()] };
Needs transforms:
browserify src/app.jsx -t [ babelify --extensions .jsx ] -t css-modulesify -o dist/bundle.js
These don’t process JSX or CSS natively. You’d need to precompile JSX to JS and handle CSS separately (e.g., via <link> tags or CSS-in-JS).
| Scenario | Best Tool |
|---|---|
| New app, modern browsers, fast DX | Vite |
| Library authoring, minimal bundles | Rollup |
| Enterprise app, full control, legacy support | Webpack |
| Quick prototype, no config | Parcel |
| Micro-frontends with mixed module formats | SystemJS |
| Legacy AMD codebase maintenance | RequireJS (migrate soon) |
| Simple CommonJS in browser, small project | Browserify |
| CDN-based ESM prototyping | JSPM |
For new projects, start with Vite unless you have specific constraints. It gives you the best balance of speed, simplicity, and modern features.
If you’re publishing a library, use Rollup — it’s the industry standard for clean, tree-shakable output.
For large, complex applications with custom requirements (e.g., code splitting strategies, asset pipelines), Webpack’s ecosystem is unmatched, despite its config burden.
Avoid RequireJS and Browserify for new work. SystemJS and JSPM solve real problems but only in specialized contexts — don’t reach for them by default.
The future is native ES modules, and tools like Vite prove you don’t need heavy bundling during development to be productive. Choose accordingly.
Choose systemjs only if you need to load multiple module formats (AMD, CommonJS, UMD, ESM) dynamically in the same browser environment, such as in micro-frontend architectures or legacy system integrations. It’s actively maintained but niche; for most new projects, native ES modules or bundlers like Vite are simpler and faster.
Choose browserify if you need to use Node.js-style require() in the browser and are working on a small-to-medium project without complex asset handling. It’s simple to set up but lacks built-in support for modern features like HMR or CSS processing, so you’ll need additional transforms. Avoid it for new projects targeting modern browsers where native ES modules are preferred.
Choose jspm if you want a package manager and module loader that works directly with native ES modules in the browser without a build step during development. It excels at dynamic imports and CDN-based workflows using its own registry. However, it has a steeper learning curve and less community tooling compared to mainstream bundlers, making it better suited for specific use cases like prototyping or micro-frontend architectures.
Choose parcel if you want zero-configuration bundling with fast builds and built-in support for HTML, CSS, assets, and modern JavaScript. It’s ideal for prototypes, small apps, or teams that want to avoid complex config files. However, customization can be harder when you need fine-grained control over the build process, and large projects may hit performance limits.
Do not use requirejs in new projects. It was designed for AMD (Asynchronous Module Definition) in pre-ES6 environments and is now largely obsolete. Modern browsers support native ES modules, and current tooling assumes ESM or CommonJS. If maintaining a legacy AMD codebase, consider migrating to a modern bundler instead of starting fresh with RequireJS.
Choose rollup if you’re building libraries or applications that benefit from tree-shaking and minimal output bundles. It’s widely used in the ecosystem (e.g., Vue, Svelte) and produces clean, efficient code by default. However, it requires plugins for non-JS assets and lacks built-in dev server features, so it’s less ideal for full application development without additional setup.
Choose vite if you want near-instant startup and hot reloads by leveraging native ES modules in development and using rollup for production builds. It supports modern frameworks out of the box, TypeScript, CSS preprocessing, and more with minimal config. It’s ideal for new projects targeting modern browsers and developers who value fast feedback loops.
Choose webpack if you need maximum flexibility and control over every aspect of your build pipeline, especially for large, complex applications with diverse asset types. Its rich plugin ecosystem handles almost any scenario, but this comes with significant configuration overhead. It remains a solid choice for enterprise apps requiring custom optimizations or legacy browser support.
SystemJS is a hookable, standards-based module loader. It provides a workflow where code written for production workflows of native ES modules in browsers (like Rollup code-splitting builds), can be transpiled to the System.register module format to work in older browsers that don't support native modules, running almost-native module speeds while supporting top-level await, dynamic import, circular references and live bindings, import.meta.url, module types, import maps, integrity and Content Security Policy with compatibility in older browsers back to IE11.
Support SystemJS by becoming a sponsor. Your logo will show up here with a link to your website.
Thank you to all our backers! 🙏 [Become a backer]
The minimal 2.8KB s.js production loader includes the following features:
System.register modules, the CSP-compatible SystemJS module format.<script type="systemjs-importmap">.The 4.2KB system.js loader adds the following features in addition to the s.js features above:
The system-node.cjs loader is a version of SystemJS build designed to run in Node.js, typically for workflows where System modules need to be executed on the server like SSR. It has the following features:
file:// urls) or the network, with included caching that respects the Content-Type header.applyImportMap api).Loading CommonJS modules is not currently supported in this loader and likely won't be. If you find you need them it is more advisable to use Node.js native module support where possible instead of the SystemJS Node.js loader.
The following pluggable extras can be dropped in with either the s.js or system.js loader:
Window.define which is created).System.register('name', ...) named bundles which can then be imported as System.import('name') (as well as AMD named define support)The following extras are included in system.js loader by default, and can be added to the s.js loader for a smaller tailored footprint:
System.import('//unpkg.com/lodash')..css, .wasm, .json module type loading support in line with the existing modules specifications.Since all loader features are hookable, custom extensions can be easily made following the same approach as the bundled extras. See the hooks documentation for more information.
To support easy loading of TypeScript or ES modules in development SystemJS workflows, see the SystemJS Babel Extension.
SystemJS does not support direct integration with the native ES module browser loader because there is no way to share dependencies between the module systems. For extending the functionality of the native module loader in browsers, see ES module Shims, which like SystemJS, provides workflows for import maps and other modules features, but on top of base-level modules support in browsers, which it does using a fast Wasm-based source rewriting to remap module specifiers.
SystemJS is designed for production modules performance roughly only around a factor of 1.5 times the speed of native ES modules, as seen in the following performance benchmark, which was run by loading 426 javascript modules (all of @babel/core) on a Macbook pro with fast wifi internet connection. Each test was the average of five page loads in Chrome 80.
| Tool | Uncached | Cached |
|---|---|---|
| Native modules | 1668ms | 49ms |
| SystemJS | 2334ms | 81ms |
The systemjs-examples repo contains a variety of examples demonstrating how to use SystemJS.
npm install systemjs
You can load System.register modules with a script element in your HTML:
<script src="system.js"></script>
<script type="systemjs-module" src="/js/main.js"></script>
<script type="systemjs-module" src="import:name-of-module"></script>
You can also dynamically load modules at any time with System.import():
System.import('/js/main.js');
where main.js is a module available in the System.register module format.
For an example of a bundling workflow, see the Rollup Code Splitting starter project - https://github.com/rollup/rollup-starter-code-splitting.
Note that when building System modules you typically want to ensure anonymous System.register statements like:
System.register([], function () { ... });
are emitted, as these can be loaded in a way that behaves the same as normal ES modules, and not named register statements like:
System.register('name', [], function () { ... });
While these can be supported with the named register extension, this approach is typically not recommended for modern modules workflows.
Say main.js depends on loading 'lodash', then we can define an import map:
<script src="system.js"></script>
<script type="systemjs-importmap">
{
"imports": {
"lodash": "https://unpkg.com/lodash@4.17.10/lodash.js"
}
}
</script>
<!-- Alternatively:
<script type="systemjs-importmap" src="path/to/map.json" crossorigin="anonymous"></script>
-->
<script type="systemjs-module" src="/js/main.js"></script>
IE11 continues to be fully supported, provided the relevant polyfills are available.
The main required polyfill is a Promise polyfill. If using import maps a fetch polyfill is also needed.
Both of these can be loaded conditionally using for example using Bluebird Promises and the GitHub Fetch Polyfill over Unpkg:
<script>
if (typeof Promise === 'undefined')
document.write('<script src="https://unpkg.com/bluebird@3.7.2/js/browser/bluebird.core.min.js"><\/script>');
if (typeof fetch === 'undefined')
document.write('<script src="https://unpkg.com/whatwg-fetch@3.4.1/dist/fetch.umd.js"><\/script>');
</script>
located before the SystemJS script itself. The above will ensure these polyfills are only fetched for older browsers without Promise and fetch support.
When using external import maps (those with src="" attributes), there is an IE11-specific workaround that might need to be used. Browsers should not make a network request when they see <script type="systemjs-importmap" src="/importmap.json"></script> during parsing of the initial HTML page. However, IE11 does so. Codesandbox demonstration
Normally this is not an issue, as SystemJS will make an additional request via fetch/xhr for the import map. However, a problem can occur when the file is cached after the first request, since the first request caused by IE11 does not send the Origin request header by default. If the request requires CORS, the lack of an Origin request header causes many web servers (including AWS Cloudfront) to omit the response CORS headers. This can result in the resource being cached without CORS headers, which causes the later SystemJS fetch() to fail because of CORS checks.
This can be worked around by adding crossorigin="anonymous" as an attribute to the <script type="systemjs-importmap"> script.
A list of projects that use or work with SystemJS in providing modular browser workflows. Post a PR.
Code-splitting builds on top of native ES modules, like Rollup offers, are an alternative to the Webpack-style chunking approach - offering a way to utilize the native module loader for loading shared and dynamic chunks instead of using a custom registry and loader as Webpack bundles include. Scope-level optimizations can be performed on ES modules when they are combined, while ensuring no duplicate code is loaded through dynamic loading and code-sharing in the module registry, using the features of the native module loader and its dynamic runtime nature.
systemjs-webpack-interop is a community-maintained npm library that might help you get webpack and systemjs working well together.
As of webpack@4.30.0, it is now possible to compile webpack bundles to System.register format, by modifying your webpack config:
{
output: {
libraryTarget: 'system',
}
}
If using webpack@<5, the following config is needed to avoid rewriting references to the global System variable:
{
module: {
rules: [
{ parser: { system: false } }
]
}
}
Third party libraries and npm packages may be used as long as they are published in a supported module format. For packages that do not exist in a supported module format, here is a list of github repos that publish System.register versions of popular third party libraries (such as react, react-dom, rxjs, etc).
Project bug fixes and changes are welcome for discussion, provided the project footprint remains minimal.
Task running is handled by Chomp (https://chompbuild.com).
To run the tests:
npm install -g chomp
chomp test
For the changelog, see CHANGELOG.md.
MIT