Meteor 1.7
Ben Newman
Meteor Night
30 May 2018
Meteor 1.7
Ben Newman
Meteor Night
30 May 2018
Different Bundles for Different Folks
Meteor 1.7
Ben Newman
Meteor Night
30 May 2018
One Size Need Not Fit All
Meteor 1.7
Ben Newman
Meteor Night
30 May 2018
Running with Safety Scissors
Meteor 1.7
Ben Newman
Meteor Night
30 May 2018
The Evergreen Dream
Three years ago
I promised:
"Our litmus test for whether a feature is worth supporting... is that if we start supporting a new language feature... like, say, classes, and you start writing code using classes, you should not have to rewrite that code when native support finally catches up." (10:15)
Three
Years
Later
Has native support caught up yet?
Can we stop transpiling yet?
The future is already here—it's just not very evenly distributed.
William Gibson (c. 1993)
Just how unevenly distributed is this future?
Possibility #1
Each browser vendor went its own way, at its own pace, with its own meandering history of partial ECMAScript support.
Possibility #2
A single browser vendor emerged victorious, and now sets the de facto standard for web compatibility.
Possibility #3
Browser vendors mostly agree on JavaScript and web standards, but many users are stuck on old browser versions.
Possibility #4
A new class of evergreen browsers has emerged, leaving nevergreen browsers increasingly far behind.
What makes a browser evergreen?
-
Keeps itself updated automatically
-
Supports all standard ECMAScript features
-
Provides feedback to the TC39 standards process
Evergreen Exemplars
Nevergreen Sluggards
If you're still building one client bundle for all supported browsers...
… you're holding the majority of your users hostage to the needs of ancient Internet Explorer and Safari versions!
And yet!
That is exactly what most frameworks and applications do, even if they use
@babel/preset-env
for configuration
The alternative is daunting
Not only must you build multiple JavaScript and CSS bundles for different browsers, with different dependency graphs and compilation rules and webpack configurations, but your server must also be able to detect the capabilities of each visiting client, so that it can deliver the appropriate assets at runtime.
The alternative is daunting
Testing a matrix of different browsers and application versions gets cumbersome quickly, so it's no surprise that responsible web developers would rather ship a single, well-tested bundle, and forget about taking advantage of modern features until legacy browsers have disappeared completely.
Ontology matters
-
Different bundles for different browsers? What about older versions?
-
Different bundles for different ECMAScript feature sets?
-
Same compilation, different supporting polyfills?
-
Everything generated on the fly?
Two key questions:
-
How many different bundles are you willing to test?
-
What native features matter to you?
How many bundles are you willing to test?
-
The fewer the better
-
One is always a valid answer!
-
One bundle can serve many browsers
-
Every distinct bundle has to be tested separately, no matter how similar to other bundles
What native features matter to you?
-
What features do you actually use?
-
What features would you use if there were fewer drawbacks?
-
How gross is the transpiled code?
-
Is the native version really faster?
-
Does one feature enable better simulations of other features?
Back in the Meteor 1.2 days
-
Almost nothing was natively supported, by any browser
-
So transpilation made sense in every browser
-
Testing any Babel-compiled code was enough of a challenge
New in Meteor 1.7
-
Two bundles:
-
One bundle tailored just for modern browsers
-
Another bundle that works for all supported browsers, both modern and legacy
-
-
Modern means:
-
native
async
/await
-
But wasn't async
/await
just standardized?
Yes, and already almost 85% of the world has access to a browser with native async
/await
support!
85% is not enough to justify ignoring the remaining 15%
-
The hold-outs will die hard!
-
The good news: modern browsers are not some small niche, but the vast majority of your users
-
Many other ECMAScript features are implied by native
async
/await
Maybe async
/await
was too strict of a requirement?
What if we aimed a bit lower?
🤔
Lesson:
You can't aim low enough to make IE10 happy, so you might as well aim high
Meteor 1.7 lets you customize the modern/legacy system to your taste
In Meteor packages
In package.js
:
Package.onUse(api => {
api.mainModule("client.js", "client");
api.mainModule("server.js", "server");
});
The current way of dividing client and server logic
In Meteor packages
In package.js
:
Package.onUse(api => {
api.mainModule("modern.js", "web.browser");
api.mainModule("legacy.js", "legacy");
api.mainModule("server.js", "server");
});
The new way of dividing client logic between the modern and legacy bundles
In Meteor applications
{
"name": "your-app",
"meteor": {
"mainModule": {
"client": "client/main.js",
"server": "server/main.js"
}
}
}
In
package.json
:
No more imports/
directory! 🎉 🎈 ✨
In Meteor applications
{
"name": "your-app",
"meteor": {
"mainModule": {
"web.browser": "client/modern.js",
"legacy": "client/legacy.js",
"server": "server/main.js"
}
}
}
In package.json
:
Different entry points for modern/legacy
Enforce your assumptions!
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
Enforce your assumptions!
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
edge: 12,
ie: Infinity,
mobileSafari: [9, 2],
opera: 36,
safari: 9,
electron: 1,
}, );
Enforce your assumptions!
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
edge: 12,
ie: Infinity,
mobileSafari: [9, 2],
opera: 36,
safari: 9,
electron: 1,
}, "classes");
Enforce your assumptions!
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
edge: 12,
ie: Infinity, // Sorry, IE11.
mobileSafari: [9, 2],
opera: 36,
safari: 9,
electron: 1,
}, "classes");
Enforce your assumptions!
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
edge: 12,
ie: Infinity, // Sorry, IE11.
mobileSafari: [9, 2], // 9.2.0+
opera: 36,
safari: 9,
electron: 1,
}, "classes");
The minimum modern version for each browser is the maximum of all versions passed to setMinimumBrowserVersions
for that browser.
Meteor.isModern
A boolean flag you can consult in code shared by modern and legacy bundles
if (Meteor.isModern) {
// Do something that's only safe in modern browsers
} else {
// Do something that's safe in all browsers
}
Avoid Meteor.isModern
if you can: modern
syntax can't be guarded, and both code paths remain in both bundles
Totally different Babel configurations
-
Legacy plugins used by the
babel-compiler
package -
Modern plugins with minimum versions
-
Every natively supported feature is one less feature to compile for the modern bundle
Hidden costs of transpilation
// What's wrong here?
let promise = new Promise((resolve, reject) => {
this.savePromise(promise, resolve, reject);
});
promise
variable uninitialized when used, but no TDZ error since
let
compiled to
var
Hidden costs of transpilation
// Silently "succeeds" with var promise = ...
var promise = new Promise((resolve, reject) => {
this.savePromise(promise, resolve, reject);
});
promise
variable uninitialized when used, but no TDZ error since
let
compiled to
var
Hidden costs of transpilation
// Silently "succeeds" with var promise = ...
var promise = new Promise((resolve, reject) => {
this.savePromise(promise, resolve, reject);
});
promise
variable uninitialized when used, but no TDZ error since
let
compiled to
var
Hidden costs of transpilation
// Correct implementation:
let resolve, reject;
let promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
this.savePromise(promise, resolve, reject);
Inspired by true events.
Totally different runtime polyfills
require("core-js/modules/es6.object.is");
require("core-js/modules/es6.function.name");
require("core-js/modules/es6.number.is-finite");
require("core-js/modules/es6.number.is-nan");
require("core-js/modules/es7.array.flatten");
require("core-js/modules/es7.array.flat-map");
require("core-js/modules/es7.object.values");
require("core-js/modules/es7.object.entries");
require("core-js/modules/es7.object.get-own-property-descriptors");
require("core-js/modules/es7.string.pad-start");
require("core-js/modules/es7.string.pad-end");
setMinimumBrowserVersions({
chrome: 49,
edge: 12,
ie: 12,
firefox: 45,
mobileSafari: 10,
opera: 38,
safari: 10,
electron: [1, 6],
}, module.id);
Legacy versus modern ecmascript-runtime-client
:
Much less runtime feature detection!
Modern bundle also never needs to import regenerator-runtime
😌🙏🍻👟🎉
Bundle Size Impact
meteor --production --extra-packages bundle-visualizer
Asset delivery
The hard part
Asset delivery
-
You've generated different bundles
-
You've defined which browsers qualify as modern
-
How do you actually deliver different assets to different clients?
-
Not just the JS bundle, but the initial HTML response, dynamic
import()
s, CSS, and any other static assets!
User Agent sniffing
-
Not as bad as it sounds!
-
Our task is much simpler than accurately identifying every client
-
Every browser can handle the legacy bundle, so we can always fall back if not 100% confident
-
At worst, a fake user agent receives the modern bundle by mistake
Best of all:
Meteor handles every aspect of modern/legacy asset delivery completely automatically
Only possible because Meteor manages its own web server
Everyone in the Meteor community uses the same system
Asset delivery
The easy part
What if something goes horribly wrong?
-
You're not alone!
-
Several options:
-
Make modern requirements stricter
-
Improve legacy compilation/polyfill strategy (additional code: 😎)
-
Fix modern compilation/polyfill strategy (additional code: 😬)
-
This does not mean you should stop caring about legacy bundle size!
If you can stop using an entire npm package on the client, that immediately benefits both modern and legacy users
meteor create --minimal my-minimal-app
Case study:
Isomorphic fetch()
polyfill
Isomorphic fetch()
polyfill
Package.describe({
name: "fetch",
version: "0.1.0",
summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()",
documentation: "README.md"
});
In
package.js
:
Isomorphic fetch()
polyfill
Package.describe({
name: "fetch",
version: "0.1.0",
summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()",
documentation: "README.md"
});
Package.onUse(function(api) {
});
In
package.js
:
Isomorphic fetch()
polyfill
Package.describe({
name: "fetch",
version: "0.1.0",
summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()",
documentation: "README.md"
});
Package.onUse(function(api) {
api.mainModule("modern.js", "web.browser");
api.mainModule("legacy.js", "legacy");
api.mainModule("server.js", "server");
});
In
package.js
:
Isomorphic fetch()
polyfill
Package.describe({
name: "fetch",
version: "0.1.0",
summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()",
documentation: "README.md"
});
Npm.depends({
"node-fetch": "2.1.2",
"whatwg-fetch": "2.0.4"
});
Package.onUse(function(api) {
api.mainModule("modern.js", "web.browser");
api.mainModule("legacy.js", "legacy");
api.mainModule("server.js", "server");
});
In
package.js
:
Isomorphic fetch()
polyfill
Package.describe({
name: "fetch",
version: "0.1.0",
summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()",
documentation: "README.md"
});
Npm.depends({
"node-fetch": "2.1.2",
"whatwg-fetch": "2.0.4"
});
Package.onUse(function(api) {
api.use("modules");
api.use("modern-browsers");
api.use("promise");
api.mainModule("modern.js", "web.browser");
api.mainModule("legacy.js", "legacy");
api.mainModule("server.js", "server");
});
In
package.js
:
Isomorphic fetch()
polyfill
Package.describe({
name: "fetch",
version: "0.1.0",
summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()",
documentation: "README.md"
});
Npm.depends({
"node-fetch": "2.1.2",
"whatwg-fetch": "2.0.4"
});
Package.onUse(function(api) {
api.use("modules");
api.use("modern-browsers");
api.use("promise");
api.mainModule("modern.js", "web.browser");
api.mainModule("legacy.js", "legacy");
api.mainModule("server.js", "server");
api.export("fetch");
});
In
package.js
:
Isomorphic
fetch()
polyfill
exports.fetch = global.fetch;
exports.Headers = global.Headers;
exports.Request = global.Request;
exports.Response = global.Response;
In
modern.js
:
No need for any polyfill; just re-export the global fetch() implementation!
Isomorphic
fetch()
polyfill
require("whatwg-fetch");
exports.fetch = global.fetch;
exports.Headers = global.Headers;
exports.Request = global.Request;
exports.Response = global.Response;
In
legacy.js
:
Import
whatwg-fetch
, then re-export the global
fetch()
implementation
Isomorphic
fetch()
polyfill
const fetch = require("node-fetch");
exports.fetch = fetch;
exports.Headers = fetch.Headers;
exports.Request = fetch.Request;
exports.Response = fetch.Response;
In
server.js
:
Import
node-fetch
, then re-export the global
fetch()
implementation
Isomorphic fetch()
polyfill
const fetch = require("node-fetch");
exports.fetch = fetch;
exports.Headers = fetch.Headers;
exports.Request = fetch.Request;
exports.Response = fetch.Response;
const { setMinimumBrowserVersions } = require("meteor/modern-browsers");
In
server.js
:
Isomorphic fetch()
polyfill
const fetch = require("node-fetch");
exports.fetch = fetch;
exports.Headers = fetch.Headers;
exports.Request = fetch.Request;
exports.Response = fetch.Response;
const { setMinimumBrowserVersions } = require("meteor/modern-browsers");
// https://caniuse.com/#feat=fetch
setMinimumBrowserVersions({
chrome: 42,
edge: 14,
firefox: 39,
mobileSafari: [10, 3],
opera: 29,
safari: [10, 1],
phantomjs: Infinity,
// https://github.com/Kilian/electron-to-chromium/blob/master/full-versions.js
electron: [0, 25],
}, module.id);
In
server.js
:
Isomorphic fetch()
polyfill
Isomorphic
fetch()
polyfill
-
One command to enable this polyfill:
meteor add fetch
-
Near zero cost for modern bundle
-
Isomorphic across server, modern and legacy bundles
-
Modern/legacy distinction is purely an implementation detail
Benefits:
Why haven't other frameworks done this yet?
Requires a cooperating webserver
npm packages are precompiled
Meteor's package system still provides several essential super-powers
Ember is considering similar functionality
Best of luck to everyone else!
Selective compilation of node_modules
-
Useful if the npm package author neglected to support older browsers
-
The only way to deliver different assets to different browsers!
-
Extremely contentious unsolved problem in the JS community
-
Ongoing discussions
-
Selective compilation of
node_modules
How Meteor 1.7 makes it possible:
-
Clone the package repository into your application's
imports
directory -
Make any modifications necessary
-
Then use
npm install
to link the package intonode_modules
Selective compilation of
node_modules
How Meteor 1.7 makes it possible:
# Clone the offending package as a git submodule
git submodule add \
git@github.com:visionmedia/superagent.git \
imports/superagent
# Tweak imports/superagent, and commit any changes locally
# Creates a symbolic link at node_modules/superagent
meteor npm install imports/superagent
Often no changes will be necessary!
What do we really mean by "zero configuration"?
-
Eliminate the need for configuration
-
Picking reasonable defaults is not enough unless they work for everyone
-
Avoid centralized configuration
-
Automate reconfiguration
How to update
-
From scratch: meteor.com/install
-
Existing apps:
-
meteor update --release 1.7.0.1
-
-
Also important:
-
meteor npm install @babel/runtime@latest
-
meteor npm install meteor-node-stubs@latest
-
-
Release notes: History.md
Questions?
Meteor 1.7
By Ben Newman
Meteor 1.7
- 4,966