Meteor 1.7

Ben Newman

Meteor Night

30 May 2018

{ github,
  twitter,
  instagram,
  facebook
}.com/benjamn 

Meteor 1.7

Ben Newman

Meteor Night

30 May 2018

{ github,
  twitter,
  instagram,
  facebook
}.com/benjamn 

Different Bundles for Different Folks

Meteor 1.7

Ben Newman

Meteor Night

30 May 2018

{ github,
  twitter,
  instagram,
  facebook
}.com/benjamn 

One Size Need Not Fit All

Meteor 1.7

Ben Newman

Meteor Night

30 May 2018

{ github,
  twitter,
  instagram,
  facebook
}.com/benjamn 

Running with Safety Scissors

Meteor 1.7

Ben Newman

Meteor Night

30 May 2018

{ github,
  twitter,
  instagram,
  facebook
}.com/benjamn 

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%

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

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

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  into node_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

  • 5,016