Skip to content

Instantly share code, notes, and snippets.

@morewry
Last active April 7, 2021 01:50
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save morewry/6773dd2ba84b8a4b829ae8d2b82891b3 to your computer and use it in GitHub Desktop.
Save morewry/6773dd2ba84b8a4b829ae8d2b82891b3 to your computer and use it in GitHub Desktop.
Rollup Bundle Stats of "Hello, World" Web Components with SkateJS renderer options. Your approximate bundle size starting point before adding your own code.

Bundle Sizes

Your mileage will vary.

React

┌────────────────────────────────────┐
│                                    │
│   Destination: dist/index.umd.js   │
│   Bundle Size:  126.8 KB           │
│   Minified Size:  113.27 KB        │
│   Gzipped Size:  35.76 KB          │
│                                    │
└────────────────────────────────────┘
-----------------------------
Rollup File Analysis
-----------------------------
bundle size:    128.053 KB
original size:  132.392 KB
code reduction: 3.28 %
module count:   30

HyperHTML

┌────────────────────────────────────┐
│                                    │
│   Destination: dist/index.umd.js   │
│   Bundle Size:  69.44 KB           │
│   Minified Size:  21.38 KB         │
│   Gzipped Size:  8.24 KB           │
│                                    │
└────────────────────────────────────┘
-----------------------------
Rollup File Analysis
-----------------------------
bundle size:    66.381 KB
original size:  76.633 KB
code reduction: 13.38 %
module count:   42

Snabbdom

┌────────────────────────────────────┐
│                                    │
│   Destination: dist/index.umd.js   │
│   Bundle Size:  44.21 KB           │
│   Minified Size:  15.77 KB         │
│   Gzipped Size:  5.56 KB           │
│                                    │
└────────────────────────────────────┘
-----------------------------
Rollup File Analysis
-----------------------------
bundle size:    42.38 KB
original size:  52.081 KB
code reduction: 18.63 %
module count:   31

Preact

┌────────────────────────────────────┐
│                                    │
│   Destination: dist/index.umd.js   │
│   Bundle Size:  32.8 KB            │
│   Minified Size:  14.77 KB         │
│   Gzipped Size:  5.36 KB           │
│                                    │
└────────────────────────────────────┘
-----------------------------
Rollup File Analysis
-----------------------------
bundle size:    31.397 KB
original size:  34.905 KB
code reduction: 10.05 %
module count:   17

LitHTML

┌────────────────────────────────────┐
│                                    │
│   Destination: dist/index.umd.js   │
│   Bundle Size:  51.27 KB           │
│   Minified Size:  13.91 KB         │
│   Gzipped Size:  4.66 KB           │
│                                    │
└────────────────────────────────────┘
-----------------------------
Rollup File Analysis
-----------------------------
bundle size:    49.604 KB
original size:  58.474 KB
code reduction: 15.17 %
module count:   27

Next Steps

  • Maybe add a mustache option.
  • Build for last 2 versions of supported browsers (not just Chrome) and use the polyfills.
  • Basic check of parse time, compile time, memory usage, numerous instances.

Takeaways So Far

Before checking, I expected LitHTML or HyperHTML to have the leanest build results. Preact and Snabbdom ended up beating HyperHTML. In comparison, React was 7.67 times as large as LitHTML.

That difference in gzip size is not decisively significant in any way given that time to transfer 35.76kb on a 3G network is still under a second (though, remember: that's on paper vs real life conditions and 300ms is a perceptible delay), but JS bundle size isn't only an issue over the network, it's also an issue once it arrives. The relationship between parse/compile time and JS bundle size is non-linear, but the relationship does exist and AFAIK is based on the unzipped file size (in this case, of 113.27kb for React vs 14.77kb for Preact). I expect that comparison to be similarly reasonable, though.

I suspect there might be ways to cut the size of both the React and HyperHTML builds, but I think it's a data point to consider that this is what I ended up with by spending roughly the same amount of time and attention on each setup.

I also thought it was interesting that most ended up bundling around 30 different modules (files, really), but then Preact had only 17 while HyperHTML had 42. This could be an approximation of the complexity of the underlying dependency tree, but I doubt it's a great one. After all, you can often organize the same things into either 30 very simple modules vs 15 less-simple-but-still-simple-enough modules. I did find, however, that when I traced Preact's imports back a few levels it was particularly easy to figure out what was being imported and why. None of the options were too far off the others in that respect, though.

import { props, withComponent } from 'skatejs';
import withHyperHtml from './renderer-hyperhtml.js';
class WithHyperHTML extends withComponent(withHyperHtml()) {
static get props() {
return {
name: props.string
};
}
render({ name }) {
return this.html`Hello, ${name}!`;
}
}
customElements.define('with-hyperhtml', WithHyperHTML);
<!doctype html>
<html>
<head>
<title>SkateJS Demo</title>
<script src="/dist/index.umd.js"></script>
</head>
<body>
<with-preact name="Preact"></with-preact>
<with-react name="React"></with-react>
<with-lit-html name="LitHTML"></with-lit-html>
<with-hyperhtml name="HyperHTML"></with-hyperhtml>
</body>
</html>
// import './preact.js';
// import './react.js';
// import './lit-html.js'
// import './hyperhtml.js';
// import './snabbdom.js';
import { props, withComponent } from 'skatejs';
import withLitHtml from '@skatejs/renderer-lit-html';
import { html } from 'lit-html';
class WithLitHtml extends withComponent(withLitHtml()) {
static get props() {
return {
name: props.string
};
}
render({ name }) {
return html`Hello, ${name}!`;
}
}
customElements.define('with-lit-html', WithLitHtml);
{
"dependencies": {
"@skatejs/renderer-lit-html": "^0.2.2",
"@skatejs/renderer-preact": "^0.3.3",
"@skatejs/renderer-react": "^0.3.1",
"@skatejs/renderer-snabbdom": "^0.0.1",
"hyperhtml": "^2.20.1",
"lit-html": "^0.14.0",
"preact": "^8.3.1",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"skatejs": "^5.2.4",
"snabbdom": "^0.7.2"
},
"description": "A SkateJS demo.",
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/plugin-transform-runtime": "^7.1.0",
"@babel/preset-env": "^7.1.6",
"@babel/preset-react": "^7.0.0",
"babel-plugin-transform-custom-element-classes": "^0.1.0",
"rollup": "^0.67.3",
"rollup-node-externals": "0.0.1-2",
"rollup-plugin-analyzer": "^2.1.0",
"rollup-plugin-babel": "^4.0.3",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-filesize": "^5.0.1",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-replace": "^2.1.0"
},
"engines": {
"node": "^10.1.0",
"npm": "^6.4.1"
},
"license": "WTFPL",
"main": "index.js",
"name": "skatejs-demo",
"private": true,
"scripts": {
"build": "rollup -c",
"build:analysis": "rollup -c --config-analysis",
"start": "echo 'using python -m SimpleHTTPServer 8888'"
},
"version": "0.1.0-initial"
}
/** @jsx h */
import { props, withComponent } from 'skatejs';
import withPreact from '@skatejs/renderer-preact';
// eslint-disable-next-line no-unused-vars
import { h } from 'preact';
class WithPreact extends withComponent(withPreact()) {
static get props() {
return {
name: props.string
};
}
render({ name }) {
return <span>Hello, {name}!</span>;
}
}
customElements.define('with-preact', WithPreact);
/** @jsx React.createElement */
import { props, withComponent } from 'skatejs';
import withReact from '@skatejs/renderer-react';
// eslint-disable-next-line no-unused-vars
import React from 'react';
// eslint-disable-next-line no-unused-vars
import * as reactDOM from 'react-dom';
class WithReact extends withComponent(withReact()) {
static get props() {
return {
name: props.string
};
}
render({ name }) {
return <span>Hello, {name}!</span>;
}
}
customElements.define('with-react', WithReact);
/* Using skatejs@^5.2.4, ported this from skatejs 6 branch */
import { bind } from 'hyperhtml';
export default (Base = HTMLElement) =>
class extends Base {
renderer(root, call) {
this.html = this.html || bind(root);
call();
}
};
// configure the renderer with the desired modules
import createRenderer from '@skatejs/renderer-snabbdom';
import snabbAttributes from 'snabbdom/es/modules/attributes';
import snabbEventListeners from 'snabbdom/es/modules/eventlisteners';
import snabbClass from 'snabbdom/es/modules/class';
import snabbProps from 'snabbdom/es/modules/props';
// import snabbStyle from 'snabbdom/es/modules/style';
// import snabbDataset from 'snabbdom/es/modules/dataset';
export default createRenderer([
snabbAttributes,
snabbEventListeners,
snabbClass,
snabbProps // ,
// snabbStyle,
// snabbDataset
]);
'use strict';
function _interopDefault(ex) {
return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex;
}
var path = require('path');
var builtins = _interopDefault(require('builtin-modules'));
var resolveId = _interopDefault(require('resolve'));
var isModule = _interopDefault(require('is-module'));
var fs = _interopDefault(require('fs'));
var ES6_BROWSER_EMPTY = path.resolve(__dirname, '../src/empty.js');
var CONSOLE_WARN = function() {
var args = [],
len = arguments.length;
while (len--) args[len] = arguments[len];
return console.warn.apply(console, args);
}; // eslint-disable-line no-console
// It is important that .mjs occur before .js so that Rollup will interpret npm modules
// which deploy both ESM .mjs and CommonJS .js files as ESM.
var DEFAULT_EXTS = ['.mjs', '.js', '.json', '.node'];
var readFileCache = {};
var readFileAsync = function(file) {
return new Promise(function(fulfil, reject) {
return fs.readFile(file, function(err, contents) {
return err ? reject(err) : fulfil(contents);
});
});
};
var statAsync = function(file) {
return new Promise(function(fulfil, reject) {
return fs.stat(file, function(err, contents) {
return err ? reject(err) : fulfil(contents);
});
});
};
function cachedReadFile(file, cb) {
if (file in readFileCache === false) {
readFileCache[file] = readFileAsync(file).catch(function(err) {
delete readFileCache[file];
throw err;
});
}
readFileCache[file].then(function(contents) {
return cb(null, contents);
}, cb);
}
var isFileCache = {};
function cachedIsFile(file, cb) {
if (file in isFileCache === false) {
isFileCache[file] = statAsync(file).then(
function(stat) {
return stat.isFile();
},
function(err) {
if (err.code == 'ENOENT') {
return false;
}
delete isFileCache[file];
throw err;
}
);
}
isFileCache[file].then(function(contents) {
return cb(null, contents);
}, cb);
}
var resolveIdAsync = function(file, opts) {
return new Promise(function(fulfil, reject) {
return resolveId(file, opts, function(err, contents) {
return err ? reject(err) : fulfil(contents);
});
});
};
function nodeResolve(options) {
if (options === void 0) options = {};
console.log(options);
var useEsnext = options.esnext !== false;
var useModule = options.module !== false;
var useMain = options.main !== false;
var useJsnext = options.jsnext === true;
var isPreferBuiltinsSet =
options.preferBuiltins === true || options.preferBuiltins === false;
var preferBuiltins = isPreferBuiltinsSet ? options.preferBuiltins : true;
var customResolveOptions = options.customResolveOptions || {};
var jail = options.jail;
var only = Array.isArray(options.only)
? options.only.map(function(o) {
return o instanceof RegExp
? o
: new RegExp(
'^' + String(o).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&') + '$'
);
})
: null;
var browserMapCache = {};
var onwarn = options.onwarn || CONSOLE_WARN;
console.log({ useModule, useMain, useJsnext, preferBuiltins, jail, only });
if (options.skip) {
throw new Error(
'options.skip is no longer supported — you should use the main Rollup `external` option instead'
);
}
if (!useModule && !useMain && !useJsnext) {
throw new Error(
'At least one of options.module, options.main or options.jsnext must be true'
);
}
var preserveSymlinks;
return {
name: 'node-resolve',
options: function options(options$1) {
preserveSymlinks = options$1.preserveSymlinks;
},
onwrite: function onwrite() {
isFileCache = {};
readFileCache = {};
},
resolveId: function resolveId$$1(importee, importer) {
if (/\0/.test(importee)) {
return null;
} // ignore IDs with null character, these belong to other plugins
// disregard entry module
if (!importer) {
return null;
}
if (options.browser && browserMapCache[importer]) {
var resolvedImportee = path.resolve(path.dirname(importer), importee);
var browser = browserMapCache[importer];
if (
browser[importee] === false ||
browser[resolvedImportee] === false
) {
return ES6_BROWSER_EMPTY;
}
if (
browser[importee] ||
browser[resolvedImportee] ||
browser[resolvedImportee + '.js'] ||
browser[resolvedImportee + '.json']
) {
importee =
browser[importee] ||
browser[resolvedImportee] ||
browser[resolvedImportee + '.js'] ||
browser[resolvedImportee + '.json'];
}
}
var parts = importee.split(/[/\\]/);
var id = parts.shift();
if (id[0] === '@' && parts.length) {
// scoped packages
id += '/' + parts.shift();
} else if (id[0] === '.') {
// an import relative to the parent dir of the importer
id = path.resolve(importer, '..', importee);
}
if (
only &&
!only.some(function(pattern) {
return pattern.test(id);
})
) {
return null;
}
var disregardResult = false;
var packageBrowserField = false;
var extensions = options.extensions || DEFAULT_EXTS;
var resolveOptions = {
basedir: path.dirname(importer),
packageFilter: function packageFilter(pkg, pkgPath) {
var pkgRoot = path.dirname(pkgPath);
if (options.browser && typeof pkg['browser'] === 'object') {
console.log({ browser: options.browser });
packageBrowserField = Object.keys(pkg['browser']).reduce(function(
browser,
key
) {
var resolved =
pkg['browser'][key] === false
? false
: path.resolve(pkgRoot, pkg['browser'][key]);
browser[key] = resolved;
if (key[0] === '.') {
var absoluteKey = path.resolve(pkgRoot, key);
browser[absoluteKey] = resolved;
if (!path.extname(key)) {
extensions.reduce(function(browser, ext) {
browser[absoluteKey + ext] = browser[key];
return browser;
}, browser);
}
}
return browser;
},
{});
}
if (options.browser && typeof pkg['browser'] === 'string') {
console.log('using browser for', pkg.name);
pkg['main'] = pkg['browser'];
} else if (useEsnext && pkg['esnext']) {
console.log('using esnext for', pkg.name);
pkg['main'] = pkg['esnext'];
} else if (useModule && pkg['module']) {
console.log('using module for', pkg.name);
pkg['main'] = pkg['module'];
} else if (useJsnext && pkg['jsnext:main']) {
console.log('using jsnext for', pkg.name);
pkg['main'] = pkg['jsnext:main'];
} else if ((useJsnext || useModule) && !useMain) {
console.log('disregardResult for', pkg.name);
disregardResult = true;
} else {
console.log('using main for', pkg.name, '?');
}
return pkg;
},
readFile: cachedReadFile,
isFile: cachedIsFile,
extensions: extensions
};
if (preserveSymlinks !== undefined) {
resolveOptions.preserveSymlinks = preserveSymlinks;
}
return resolveIdAsync(
importee,
Object.assign(resolveOptions, customResolveOptions)
)
.catch(function() {
return false;
})
.then(function(resolved) {
if (options.browser && packageBrowserField) {
if (packageBrowserField[resolved]) {
resolved = packageBrowserField[resolved];
}
browserMapCache[resolved] = packageBrowserField;
}
if (!disregardResult && resolved !== false) {
if (!preserveSymlinks && resolved && fs.existsSync(resolved)) {
resolved = fs.realpathSync(resolved);
}
if (~builtins.indexOf(resolved)) {
return null;
} else if (~builtins.indexOf(importee) && preferBuiltins) {
if (!isPreferBuiltinsSet) {
onwarn(
"preferring built-in module '" +
importee +
"' over local alternative " +
"at '" +
resolved +
"', pass 'preferBuiltins: false' to disable this " +
"behavior or 'preferBuiltins: true' to disable this warning"
);
}
return null;
} else if (
jail &&
resolved.indexOf(path.normalize(jail.trim(path.sep))) !== 0
) {
return null;
}
}
if (resolved && options.modulesOnly) {
return readFileAsync(resolved, 'utf-8').then(function(code) {
return isModule(code) ? resolved : null;
});
} else {
return resolved === false ? null : resolved;
}
});
}
};
}
module.exports = nodeResolve;
import { plugin as analysis } from 'rollup-plugin-analyzer';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import filesize from 'rollup-plugin-filesize';
import nodeExternal from 'rollup-node-externals';
import nodeResolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';
const sharedConfig = {
external: nodeExternal({
whitelist: [
'@skatejs/renderer-lit-html',
'@skatejs/renderer-preact',
'@skatejs/renderer-react',
'@skatejs/renderer-snabbdom',
'hyperhtml',
'lit-html',
'preact',
'react',
'react-dom',
'skatejs',
'snabbdom',
'snabbdom-pragma',
'snabbdom/es/modules/attributes',
'snabbdom/es/modules/eventlisteners',
'snabbdom/es/modules/class',
'snabbdom/es/modules/props',
'snabbdom/es/modules/style',
'snabbdom/es/modules/dataset',
'snabbdom/vnode'
]
}),
input: 'index.js',
plugins: [
nodeResolve({
// TODO: esnext isn't actually supported, so I temporarily added it
esnext: true,
jsnext: true,
main: true,
module: true,
browser: false
}),
babel({
babelrc: false,
include: '**/*.js',
plugins: [
// TODO: later
// 'transform-custom-element-classes',
[
'@babel/plugin-transform-runtime',
{
regenerator: false,
useESModules: true
}
]
],
presets: [
['@babel/preset-react'],
[
'@babel/preset-env',
{
targets: {
// TODO: temporary, more later
chrome: '70'
}
}
]
],
runtimeHelpers: true
}),
replace({
'process.env.NODE_ENV': JSON.stringify('production')
}),
commonjs({
namedExports: {
'react-dom': ['render', 'unmountComponentAtNode']
}
}),
filesize()
]
};
const getBaseConfig = function(options = {}) {
// npx rollup -c --config-analysis
if (options['config-analysis']) {
sharedConfig.plugins.push(analysis());
}
return sharedConfig;
};
const umdOutput = function(options) {
const baseConfig = getBaseConfig(options);
return Object.assign({}, baseConfig, {
output: {
file: 'dist/index.umd.js',
format: 'umd',
name: 'skateJsDemo'
}
});
};
export default function(options) {
return [
// TODO: esModuleOutput(options),
umdOutput(options)
];
}
/** @jsx dom.createElement */
import { props, withComponent } from 'skatejs';
import withSnabbdom from './renderer-snabbdom.js';
// eslint-disable-next-line no-unused-vars
import * as dom from 'snabbdom-pragma';
class WithSnabbdom extends withComponent(withSnabbdom()) {
static get props() {
return {
name: props.string
};
}
render({ name }) {
return <span>Hello, {name}!</span>;
}
}
customElements.define('with-snabbdom', WithSnabbdom);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment