oocscommerce / module-requirejs-optimize
Browserless RequireJS bundling for Magento 2 storefronts
Package info
github.com/oocscommerce/module-requirejs-optimize
Language:JavaScript
Type:magento2-module
pkg:composer/oocscommerce/module-requirejs-optimize
Requires
- php: >=7.2
- magento/framework: *
- magento/module-cms: *
- magento/module-deploy: *
- magento/module-store: *
- magento/module-theme: *
- magento/module-widget: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
oocscommerce/module-requirejs-optimize
Bundles the RequireJS modules of a Magento 2 storefront into a few chunks per page type, without a browser: like Vite, it finds what each page needs by reading the code, not by recording page loads.
Static analysis of layout XML, templates, PHP blocks, CMS content and widgets
→ JS entry points per page type → module graph → chunks per page type, with lazy modules
merged per lazy root. The Magento module collects the site context with Magento APIs; the
Node engine in tools/ parses JS and writes the chunks.
Features
- No browser, no crawling: entry points come from layout, templates, PHP blocks, CMS pages/blocks and widgets
- Chunks per page type (
common,product,cart_checkout, ...); mixins and single-use dependencies merged with the module that uses them - Lazy modules stay lazy, merged per lazy root with their dependencies, mixins and knockout templates
ifconfigblocks follow the store configuration- Works with
merge_files,minify_files(chunks become*.min.js) and Magento_Csp subresource integrity (2.4.7+): hashes of the changed files are saved through Magento's own SRI storage - Falls back to single files when a chunk fails to load; rebuilds do not break pages being loaded
- No
npm installon the site: the engine ships as one file
Benchmark
JS files loaded on Magento 2.4 Open Source, Luma with sample data, production mode, cold cache:
| Page | Before | After |
|---|---|---|
| Home | 194 | 12 |
| Category | 195 | 11 |
| Product | 220 | 19 |
| Search | 200 | 15 |
| Cart | 202 | 11 |
| Checkout | 255 | 11 |
Only these main pages were measured. Other pages (customer account, CMS pages, wishlist...) load fewer
files too: the code every page shares comes from one common chunk.
Installation
Requirements: production mode, Node.js 18+ on the machine that runs bin/magento,
dev/js/enable_js_bundling off.
composer require oocscommerce/module-requirejs-optimize bin/magento module:enable OOCS_RequireJsOptimize bin/magento setup:upgrade && bin/magento setup:di:compile bin/magento setup:static-content:deploy bin/magento static:optimize:init # write static-optimize.json (page types, exclude, entries) to edit bin/magento static:optimize:run # build every theme/locale used by an active store view
Usage
Run bin/magento static:optimize:run after every setup:static-content:deploy: the deploy
rewrites requirejs-config.js and its subresource integrity hash, which removes the loader.
build and clean end with a new static content version, as
setup:static-content:deploy --refresh-content-version-only does, so browsers and CDNs load the new
requirejs-config.js, unless no theme/locale was built or --no-version-refresh is passed. The full
page cache and Varnish/CDN are not purged: do it after the run.
bin/magento static:optimize:run [build|check|clean] [--theme Vendor/theme] [--locale xx_XX] [--min-chunk 0] [--max-chunk 0] [--progressive] [--json report.json] [--node /path/to/node] [--no-version-refresh] [-v] bin/magento static:optimize:init [--force] # write static-optimize.json with the default page types
build(default) writes the chunks and the loader,checkonly prints the analysis,cleanremoves them.--theme,--locale: only this theme/locale; by default every one used by an active store view.--min-chunk N: chunks shared by several page types and smaller than N KB are merged intocommon. Fewer requests, but every page then downloads that code, needed or not.0(default) never merges.--max-chunk N: chunks larger than N KB of source are written as several files, requested together. Each file is parsed and run in a shorter main-thread task (lower Total Blocking Time) at the cost of a few more requests.0(default) never splits.--progressive: bundled modules are defined in slices of about 15 ms, giving the main thread back to the browser in between, instead of in one long task (lower Total Blocking Time). The chunks are still downloaded at once; only running their modules is spread out. Off by default.--json report.json(check): full analysis as JSON.--node: Node.js binary whennodeis not in thePATH.--no-version-refresh: keep the static content version (see above). Refresh it yourself after the run: withdev/js/merge_fileson, pages keep their merged JS file (and the old loader) until the version changes, as Magento names merged files after it.-v: engine details, and keeps the context file for debugging.
Configuration: static-optimize.json
Optional, in the Magento root; bin/magento static:optimize:init writes it with the defaults.
{
"minChunk": 0,
"maxChunk": 0,
"progressive": false,
"exclude": ["Vendor_Slider/js/*"],
"entries": {"home": ["Magento_PageBuilder/js/widget-initializer"]},
"pages": {
"home": ["default", "cms_index_index", "cms_page_view"],
"wishlist": ["default", "wishlist_index_index"]
}
}
pages: page types and the layout handles Magento loads for each. Without the file the defaults are used (home, category, product simple/configurable/bundle/grouped, search, cart, checkout, success, login, account). Page types whose handles have no layout on the site are skipped.- Blocks with
ifconfig="..."in layout XML follow the store configuration of the store views using the theme/locale: flag on -> bundled like any block, off -> not rendered so its JS is ignored, different values between those store views -> its JS stays single files. Runstatic:optimize:runagain after changing such a setting. minChunk,maxChunk: defaults of--min-chunkand--max-chunk(KB,0= off); the command line options win when given.progressive:truebuilds as with--progressive.exclude: module ids never bundled (*matches anything,/included); they load as single files, e.g. a third-party module that breaks inside a bundle.entries: extra entry modules per page type for JS the static analysis cannot find (added by PHP at runtime, Page Builder...); they and their dependencies are bundled for that page type.
With -v the context file handed to the engine is kept in var/tmp/oocs_requirejsoptimize/.
Customizing the engine
PHP (Console/, Model/) collects the site context with Magento APIs; the Node engine in
tools/ does the analysis and writes the chunks. The command runs tools/dist/rjs-optimize.js,
a single file with all dependencies bundled, so sites need no npm install.
| Path | Role |
|---|---|
tools/bin/rjs-optimize.js |
CLI: check, build, clean |
tools/src/project.js |
reads the context file written by PHP |
tools/src/requirejs.js |
requirejs-config.js, module id → file resolution, mixins |
tools/src/layout.js |
layout XML, templates, PHP blocks, CMS → JS entry points |
tools/src/graph.js |
module dependency graph (acorn) |
tools/src/analyze.js |
modules per page type, chunk proposal |
tools/src/build.js |
chunk files, loader, clean |
After changing tools/src or tools/bin, rebuild the single file and commit it:
cd tools npm ci # once: acorn, fast-xml-parser, terser, esbuild npm run dist # writes dist/rjs-optimize.js
When dist/rjs-optimize.js is missing, the command runs tools/bin/rjs-optimize.js from the sources
instead, which needs npm ci. Delete the dist file while developing to skip the rebuild step.
Debugging: -v keeps the context file and prints its path, so the engine can be run on it directly
(module in vendor/oocscommerce/module-requirejs-optimize, or app/code/OOCS/RequireJsOptimize):
bin/magento static:optimize:run check -v
node vendor/oocscommerce/module-requirejs-optimize/tools/bin/rjs-optimize.js check \
--context var/tmp/oocs_requirejsoptimize/context-Magento_luma-en_US.json --json report.json
What build does
- Groups modules by the set of page types that need them (
common,product,cart_checkout, ...); mixins and single-use dependencies end up next to the module that uses them. Pages that may load a module through anifconfigblock count for the grouping. - Lazy modules (uiComponent children,
require()on demand) stay lazy but are merged per lazy root with their dependencies, mixins and templates (lazy-checkmo-method); modules shared by several roots get one shared lazy chunk (lazy-payment-default-shared: place-order + mixins). A lazy group of a single module keeps loading as its own file. - Chunks contain named defines; text templates are inlined; they are minified with terser when
the store has
dev/js/minify_fileson, like the rest of its JS. - Prepends a loader to
requirejs-config.jsthat wrapsrequire.load: requesting a bundled module loads its chunk once.require.config({bundles})is not used because it makesrequire.toUrl()return the bundle URL, which breaksmage/requirejs/mixins. - Not bundled, loaded as single files like before: non-AMD files, modules listed in
exclude, modules used only byifconfigblocks whose flag differs between store views, and anything the static analysis did not find. A chunk that fails to load falls back to single files.
--pages overrides the analysed page types, e.g. {"product": ["default", "catalog_product_view"]}.
Known gaps
- knockout templates whose path is computed at runtime
- Page Builder content types are not analysed
- JS added only by PHP code paths the static scan cannot follow loads as single files