Update: I recently spoke about this in Chennai EmberJS.
The market share of users with evergreen browsers is rising fast. Inspite of this, we ship transpiled assets, often caused by the need to support legacy browsers like IE, to everyone. This is because dropping browser support isn't an easy affair, at least in enterprises. As a result, everyone, including those having the latest versions of browsers are forced to consume a bigger and slower bundle.
While there are established ways in React(through Webpack), Meteor and Vue to achieve differential bundling to serve assets based on the browser, there is currently no official way to do it in Ember. There is a pre-RFC that is being discussed upon and I felt like writing out about a strategy that we have found to be feasible.
This strategy tries to address engines as well. With the usual type="module"
and nomodule
approach, you cannot handle engines as they rely on the asset manifest provided by a meta
tag with name as app/config/asset-manifest
. There is no way to toggle meta
tags in the frontend as of now and until Ember Engines support the storeConfigInMeta
option, you will either have to do something similar to this solution or not do differential serving of engine assets. PS: There is a long-pending PR to implement the storeConfigInMeta
option in Ember Engines.
This solution requires you to serve different bundles by manipulating the HTML at runtime in the server based on a cookie, or if possible, parsing the UA.
Steps
- In your
index.html
, adddata-for="ember"
attribute to script tags that are generated by Ember. For example,<script data-for="ember" src="{{rootURL}}assets/vendor.js"></script> <script data-for="ember" src="{{rootURL}}assets/app.js"></script>
- In your application code, run a small script to set a cookie to determine if the user is currently using a modern or legacy browser.
// Serve modern build if Promise and async/await are present let testCode = 'async() => { let p = new Promise(); await p(); }'; try { // this needs to be a new Function/eval because otherwise, // you will get Syntax Errors in the code (new Function(testCode))(); // Set a never-expiring cookie } catch(err) { // Set a cookie with a short validity(say a month) // If the user updates their browser, it will be reflected // once the cookie expires }
- In your
targets.js
file, use theprocess.env.LEGACY
flag to toggle support for legacy browsers.'use strict'; let browsers = [ 'last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions' ]; const isCI = !!process.env.CI; const isProduction = process.env.EMBER_ENV === 'production'; const isLegacyBuild = !!process.env.LEGACY; if (isCI || isProduction || isLegacyBuild) { browsers = [ 'Chrome >= 42', 'Firefox >= 39', 'Edge >= 14', 'Safari >= 10' 'ie 11' ]; } module.exports = { browsers };
- Run parallel builds with
Note: You can also run these builds in parallel. When doing so, you might often get build failures with the errorember build --environment=production --output-path=modern && LEGACY=true ember build --environment=production --output-path=legacy
Unexpected end of file ...
due to an issue in broccoli-persistent-filter. To avoid this, setprocess.env.BROCCOLI_PERSISTENT_FILTER_CACHE_ROOT
to a unique value for each build(for example, the complete path of the workspace or the current timestamp). - Run the
index.js
provided in this repo. It will generate adist
folder with anindex.html
containing combined script tags and meta tags wrapped in<MODERN>
and<LEGACY>
tags. - In your server, detect the presence of the cookie that was previously set (or use the UA) and replace the tags accordingly.
// if modern browser replaceAll('<LEGACY>.*</LEGACY>', ''); replaceAll('<MODERN>(.*)</MODERN>', '$1'); // if legacy browser replaceAll('<MODERN>.*</MODERN>', ''); replaceAll('<LEGACY>(.*)</LEGACY>', '$1');
Feel free to share your ideas or comments on this approach on GitHub or on Twitter.