├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── gen-bundles.sh
├── gulpfile.js
├── package.json
├── server
├── server.go
└── template
│ └── synthesized.html
└── src
├── bundle-index.html
├── display-results.js
├── index.html
├── moment
├── app.js
├── bundled-optimized.html
├── bundled-unoptimized.html
├── package.json
├── unbundled.html
└── webbundle.html
└── three
├── app.js
├── bundled-optimized.html
├── bundled-unoptimized.html
├── package.json
├── unbundled.html
└── webbundle.html
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | temp
3 | *.pem
4 | node_modules
5 | package-lock.json
6 | src/*/package-lock.json
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2017 Google Inc.
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Browser module loading tests
2 |
3 | This sample takes moment.js and three.js, and prepares them for loading in the
4 | browser with ECMAScript modules.
5 |
6 | ## Development
7 |
8 | ### Cloning this repository
9 |
10 | Standard stuff :) Hit the "Clone or download" button in the project's GitHub
11 | landing page and go from there.
12 |
13 | ### Installing dependencies
14 |
15 | NPM packages need to be installed on the root and both `src/` subdirectories.
16 |
17 | ```sh
18 | npm i
19 | ```
20 |
21 | Gulp needs to be available globally. You can install it by doing:
22 |
23 | ```sh
24 | npm i -g gulp
25 | ```
26 |
27 | ### Building and developing
28 |
29 | ```sh
30 | gulp build
31 | ```
32 |
33 | ### Running the HTTP server
34 |
35 | First add `cert.pem` and `key.pem` files for TLS. If you don't have these, you
36 | can use [simplehttp2server](https://github.com/GoogleChrome/simplehttp2server)
37 | to generate them for you. Place them at the root of the clone.
38 |
39 | Then:
40 |
41 | ```sh
42 | go run server/server.go
43 | ```
44 |
45 | Or, if you don't have `go` command, you can use the built in HTTP server instead:
46 |
47 | ```sh
48 | gulp serve
49 | ```
50 |
51 | HTTP server command line options:
52 | - `--http1`: serve over HTTP/1.1 instead of HTTP/2
53 | - `--push`: use HTTP/2 push when serving
54 | - `--preload`: inject `` tags for JS dependencies when serving
55 |
56 | E.g., to serve over HTTP/1.1 with preload enabled:
57 |
58 | ```sh
59 | go run server/server.go --http1 --preload
60 | ```
61 |
62 | ### Bundled / unbundled tests
63 |
64 | The bundled / unbundled test cases are served at the following URLs:
65 |
66 | * moment.js
67 | * bundled, optimized: https://localhost:44333/moment/bundled-optimized.html
68 | * bundled, unoptimized: https://localhost:44333/moment/bundled-unoptimized.html
69 | * unbundled: https://localhost:44333/moment/unbundled.html
70 | * three.js
71 | * bundled, optimized: https://localhost:44333/three/bundled-optimized.html
72 | * bundled, unoptimized: https://localhost:44333/three/bundled-unoptimized.html
73 | * unbundled: https://localhost:44333/three/unbundled.html
74 |
75 | These tests load the files only once, so the results may be noisy. At the
76 | toplevel test page https://localhost:44333/ you can run the unbundled test cases
77 | repeatedly (25 times) and see the median time.
78 |
79 | ### Synthesized module tree tests
80 |
81 | In addition to the real-world library test cases, this HTTP server provides
82 | a benchmark for artificial module tree shapes. This is served at
83 | https://localhost:44333/synthesized/ and it accepts the following query
84 | parameters:
85 |
86 | - `depth` (default: 5): height of the module dependency tree
87 | - `branch` (default: 2): number of child modules non-leaf modules have
88 | - `delay=n` (optional): sleep n milliseconds in response handler
89 | - `cacheable` (optional): make JavaScript resources cacheable
90 |
91 | E.g., this loads a module whose dependency tree is a perfect binary tree of
92 | depth 10 (2047 modules in total):
93 | https://localhost:44333/synthesized/?depth=10&branch=2
94 |
95 | Note: Currently, --push and --preload options are not supported in synthesized
96 | tests.
97 |
98 | ### [Experimental] WebBundle tests
99 |
100 | [Web Bundle](https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html)
101 | is a file format for encapsulating one or more HTTP resources. It allows
102 | distributing a large number of module scripts as a single HTTP resource.
103 |
104 | You need [WebBundle Go tools](https://github.com/WICG/webpackage/tree/master/go/bundle)
105 | to generate Web Bundles, which can be installed by this command:
106 | ```
107 | go get -u github.com/WICG/webpackage/go/bundle/cmd/...
108 | ```
109 | Then, this command will generate Web Bundles for the moment.js / three.js tests:
110 | ```
111 | ./gen-bundles.sh
112 | ```
113 |
114 | As of April 2020, Web Bundles support is implemented only in Chromium-based
115 | browsers, behind an experimental feature flag. To enable Web Bundles in Chrome,
116 | turn on `chrome://flags/#web-bundles` flag.
117 |
118 | After enabling the flag, drag and drop `samples-module-loading-comparison.wbn`
119 | into Chrome to open it. An index page will be displayed from which you can
120 | choose a benchmark to run.
121 |
122 | `gen-bundles.sh` also generates `dist/moment/momentjs.wbn` and
123 | `dist/three/threejs.wbn`. They only bundle the module scripts for each test.
124 | These can be used with `dist/{moment,three}/webbundle.html` to test
125 | [Subresource loading with Web Bundles](https://github.com/WICG/webpackage/blob/master/explainers/subresource-loading.md).
126 | (Note: this is a proposal in very early stage; experimental implementation
127 | is not landed in any browsers as of April 2020.)
128 |
--------------------------------------------------------------------------------
/gen-bundles.sh:
--------------------------------------------------------------------------------
1 | # All-in-one WebBundle bundling all files under dist/.
2 | gen-bundle -baseURL https://googlechromelabs.github.io/samples-module-loading-comparison/ \
3 | -dir dist \
4 | -primaryURL https://googlechromelabs.github.io/samples-module-loading-comparison/bundle-index.html \
5 | -o samples-module-loading-comparison.wbn
6 |
7 | # Moment.js subresource WebBundle
8 | gen-bundle -baseURL https://moment.js/ \
9 | -dir dist/moment/unbundled/ \
10 | -primaryURL https://moment.js/app.js \
11 | -o dist/moment/momentjs.wbn
12 |
13 | # Three.js subresource WebBundle
14 | gen-bundle -baseURL https://three.js/ \
15 | -dir dist/three/unbundled/ \
16 | -primaryURL https://three.js/app.js \
17 | -o dist/three/threejs.wbn
18 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Module loading benchmark sample.
4 | * Copyright 2017 Google Inc. All rights reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * https://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License
17 | *
18 | */
19 |
20 | const path = require('path');
21 | const fs = require('fs');
22 | const url = require('url');
23 |
24 | const gulp = require('gulp');
25 | const babel = require('gulp-babel');
26 | const rollup = require('rollup');
27 | const rollupEach = require('gulp-rollup-each');
28 | const nodeResolve = require('rollup-plugin-node-resolve');
29 | const rename = require('gulp-rename');
30 |
31 | const resolveFrom = require('resolve-from');
32 | const merge = require('merge-stream');
33 |
34 | const webpackStream = require('webpack-stream');
35 | const webpack = require('webpack');
36 |
37 | const spdy = require('spdy');
38 | const zlib = require('zlib');
39 |
40 | const del = require('del');
41 | const { task, series, parallel } = require('gulp');
42 |
43 | const projects = ['moment', 'three'];
44 | let files = {};
45 | const pushFiles = {};
46 | const cache = {};
47 |
48 | let preload = false;
49 | let http1 = false;
50 | let push = false;
51 |
52 |
53 | const wrapTaskFn = (fn) => (complete) => {
54 | const result = fn()
55 | if (typeof result === 'undefined') {
56 | complete()
57 | }
58 | return result
59 | }
60 |
61 | const gulpTask = (taskName, dependencies, taskFn) => {
62 | if (typeof dependencies === 'function') {
63 | taskFn = dependencies
64 | dependencies = []
65 | }
66 | console.log(taskName)
67 | if (!dependencies.length) {
68 | return task(taskName, wrapTaskFn(taskFn))
69 | }
70 | return task(taskName, series(parallel(...dependencies.map(n => task(n))), wrapTaskFn(taskFn)))
71 | }
72 |
73 | // Rollup plugin for listing all the module files.
74 | function _listModules(project) {
75 | return {
76 | ongenerate(args, rendered) {
77 | files[project] = [];
78 | args.bundle.modules.forEach(module => {
79 | const rel = path.relative(path.join(__dirname, 'src', project), module.id);
80 | files[project].push(rel);
81 | });
82 | }
83 | };
84 | }
85 |
86 | // Babel plugin for rewriting imports to browser-loadable relative paths.
87 | function _rewriteImports(project) {
88 | return function(babel) {
89 | const t = babel.types;
90 | const buildRoot = path.join(__dirname, 'src', project);
91 |
92 | return {
93 | visitor: {
94 | ImportDeclaration: function(nodePath, state) {
95 | const fileRoot = path.dirname(state.file.opts.filename);
96 |
97 | const moduleArg = nodePath.node.source;
98 | if (moduleArg && moduleArg.type === 'StringLiteral') {
99 | const source = nodePath.node.source.value;
100 |
101 | let relative = null;
102 | if (source.startsWith('./') || source.startsWith('../')) {
103 | relative = path.relative(fileRoot, resolveFrom(fileRoot, source));
104 | } else {
105 | relative = path.relative(fileRoot, resolveFrom(buildRoot, source));
106 | }
107 |
108 | // Special handling for the GLSL files in three.js.
109 | if (relative.endsWith('.glsl')) {
110 | relative = relative.replace('.glsl', '.js');
111 | }
112 |
113 | if (relative.startsWith('../')) {
114 | nodePath.node.source = t.stringLiteral(relative);
115 | } else {
116 | nodePath.node.source = t.stringLiteral('./' + relative);
117 | }
118 | }
119 | },
120 | }
121 | };
122 | }
123 | }
124 |
125 | // Transform three.js GLSL files into JS.
126 | // From three.js rollup.config.js.
127 | function _glsl() {
128 | return {
129 | transform(code, id) {
130 | if (/\.glsl$/.test(id) === false) return;
131 | var transformedCode = 'export default ' + JSON.stringify(
132 | code
133 | .replace( /[ \t]*\/\/.*\n/g, '' ) // remove //
134 | .replace( /[ \t]*\/\*[\s\S]*?\*\//g, '' ) // remove /* */
135 | .replace( /\n{2,}/g, '\n' ) // # \n+ to \n
136 | ) + ';';
137 | return {
138 | code: transformedCode,
139 | map: { mappings: '' }
140 | };
141 | }
142 | };
143 | }
144 |
145 | // Delete all generated files.
146 | gulpTask('clean', () => del(['dist', 'temp']));
147 |
148 | // Obtain list of dependency JS files.
149 | gulpTask('scan', () => {
150 | return Promise.all(projects.map(project => {
151 | return rollup.rollup({
152 | entry: path.join('src', project, 'app.js'),
153 | plugins: [
154 | nodeResolve(),
155 | _glsl(),
156 | _listModules(project)
157 | ],
158 | }).then(bundle => bundle.generate({ format: 'es' }));
159 | }));
160 | });
161 |
162 | // Rename GLSL files, transform them, and move them together with the other JS.
163 | // Special handling for the GLSL files in three.js.
164 | gulpTask('glsl', ['scan'], () => {
165 | const tasks = ['three'].map(project => {
166 | const glslFiles = files[project]
167 | .filter(f => /\.glsl$/.test(f))
168 | .map(f => path.join(__dirname, 'src', project, f));
169 | return gulp.src(glslFiles, {base: path.join(__dirname, 'src', project)})
170 | .pipe(rollupEach({
171 | plugins: [_glsl()]
172 | }, {
173 | format: 'es'
174 | }))
175 | .pipe(rename(path => path.extname = '.js'))
176 | .pipe(gulp.dest(path.join('temp', project, 'unbundled')));
177 | });
178 | return merge(...tasks);
179 | });
180 |
181 | // Copy HTML to all three builds.
182 | gulpTask('html', ['clean'], () => {
183 | return merge(gulp.src('src/**/*.html').pipe(gulp.dest('dist')),
184 | gulp.src('src/*.js').pipe(gulp.dest('dist')));
185 | });
186 |
187 | // Create unbundled build.
188 | gulpTask('unbundled', ['glsl', 'html'], () => {
189 | const tasks = projects.map(project => {
190 | const jsFiles = files[project]
191 | .filter(f => /\.js$/.test(f))
192 | .map(f => path.join(__dirname, 'src', project, f));
193 |
194 | return gulp.src(jsFiles, {base: path.join(__dirname, 'src', project)})
195 | .pipe(babel({
196 | babelrc: false,
197 | plugins: [_rewriteImports(project)]
198 | }))
199 | .pipe(gulp.dest(path.join('temp', project, 'unbundled')));
200 | });
201 | return merge(...tasks);
202 | });
203 |
204 | // Create optimized bundled build.
205 | gulpTask('bundled-optimized', ['unbundled'], () => {
206 | return Promise.all(projects.map(project => {
207 | return rollup.rollup({
208 | entry: path.join('temp', project, 'unbundled', 'app.js'),
209 | treeshake: true,
210 | })
211 | .then(bundle => {
212 | bundle.write({
213 | format: 'iife',
214 | moduleName: 'app',
215 | dest: path.join('temp', project, 'bundled-optimized', 'app.js'),
216 | });
217 | });
218 | }));
219 | });
220 |
221 | // Create unoptimized bundled build.
222 | gulpTask('bundled-unoptimized', ['unbundled'], () => {
223 | const tasks = projects.map(project => {
224 | return gulp.src(path.join('temp', project, 'unbundled', 'app.js'),
225 | {base: path.join('temp', project, 'unbundled')})
226 | .pipe(webpackStream({
227 | output: {
228 | filename: 'app.js'
229 | },
230 | plugins: [
231 | new webpack.LoaderOptionsPlugin({
232 | minimize: false,
233 | debug: false
234 | })
235 | ]
236 | }, webpack))
237 | .pipe(gulp.dest(path.join('temp', project, 'bundled-unoptimized')));
238 | });
239 | return merge(...tasks);
240 | });
241 |
242 | // Minify all three builds.
243 | gulpTask('minify', ['unbundled', 'bundled-unoptimized', 'bundled-optimized'], () => {
244 | const tasks = projects.map(project => {
245 | return gulp.src(`temp/${project}/**/*.js`, {base: path.join('temp', project)})
246 | .pipe(babel({
247 | babelrc: false,
248 | presets: [
249 | ['minify', {
250 | builtIns: false,
251 | evaluate: false,
252 | mangle: false,
253 | }]
254 | ],
255 | // presets: ['babili'],
256 | }))
257 | .pipe(gulp.dest(path.join('dist', project)));
258 | });
259 | return merge(...tasks);
260 | });
261 |
262 | // Meta build task for creating all builds.
263 | // Also generates JSON file with full list of served JS files.
264 | gulpTask('build', ['html', 'minify'], () => {
265 | const jsFiles = {};
266 | projects.forEach(project => {
267 | jsFiles[project] = [];
268 | files[project].forEach(file => {
269 | // This is a bit hacky, but we need it to work with the three.js custom build.
270 | if (/\.glsl$/.test(file)) {
271 | jsFiles[project].push(file.replace(/\.glsl$/, '.js'));
272 | } else {
273 | jsFiles[project].push(file);
274 | }
275 | });
276 | });
277 | fs.writeFileSync(path.join(__dirname, 'dist', 'filelist.json'), JSON.stringify(jsFiles));
278 | });
279 |
280 | // Auxiliary method for loading all served content into memory.
281 | function _cacheEverything() {
282 | const filelist = JSON.parse(fs.readFileSync(path.join(__dirname, 'dist', 'filelist.json')));
283 |
284 | console.log('HTTP server: loading files into memory...');
285 | projects.forEach(project => {
286 | const jsFiles = filelist[project];
287 | const unbundledHtml = path.join(project, 'unbundled.html');
288 | const unbundledJs = path.join(project, 'unbundled', 'app.js');
289 |
290 | let unbundledHtmlContent = fs.readFileSync(path.join(__dirname, 'dist', unbundledHtml));
291 | if (preload) {
292 | let unbundledHtmlString = unbundledHtmlContent.toString();
293 | let links = '';
294 | jsFiles.slice(0).reverse().forEach(file => {
295 | const relative = path.join(project, 'unbundled', file);
296 | links += ` \n`;
297 | });
298 | unbundledHtmlString = unbundledHtmlString.replace('', `${links}`);
299 | unbundledHtmlContent = Buffer.from(unbundledHtmlString);
300 | }
301 | cache[unbundledHtml] = zlib.gzipSync(unbundledHtmlContent);
302 | pushFiles[unbundledJs] = [];
303 | jsFiles.forEach(file => {
304 | const relative = path.join(project, 'unbundled', file);
305 | cache[relative] = zlib.gzipSync(fs.readFileSync(path.join(__dirname, 'dist', relative)));
306 | if (relative !== unbundledJs) {
307 | pushFiles[unbundledJs].push(relative);
308 | }
309 | });
310 |
311 | ['bundled-unoptimized', 'bundled-optimized'].forEach(c => {
312 | const html = path.join(project, `${c}.html`);
313 | const js = path.join(project, c, 'app.js');
314 | cache[html] = zlib.gzipSync(fs.readFileSync(path.join(__dirname, 'dist', html)));
315 | cache[js] = zlib.gzipSync(fs.readFileSync(path.join(__dirname, 'dist', js)));
316 | });
317 | });
318 | cache['index.html'] = zlib.gzipSync(fs.readFileSync(path.join(__dirname, 'dist', 'index.html')));
319 | cache['display-results.js'] = zlib.gzipSync(fs.readFileSync(path.join(__dirname, 'dist', 'display-results.js')));
320 | console.log('HTTP server: done loading.');
321 | }
322 |
323 | // Auxiliary method for handling an HTTP request.
324 | function _onRequest(request, response) {
325 | let url = request.url;
326 | if (url.endsWith('/')) {
327 | url += 'index.html';
328 | }
329 | url = url.replace(/^\/(r\/\d+\/)?/, ''); // Strip randomized prefix (/r/[0-9]+)
330 |
331 | console.log(`HTTP server: request for ${request.url}`);
332 | if (cache[url]) {
333 | if (push && pushFiles[url] && response.push) {
334 | // Reverse order so that main dependency comes first.
335 | pushFiles[url].slice(0).reverse().forEach(file => {
336 | console.log(`HTTP server: pushing /${file}`);
337 | const pushed = response.push(`/${file}`, {
338 | status: 200,
339 | method: 'GET',
340 | request: {
341 | accept: '*/*'
342 | },
343 | response: {
344 | 'content-type': 'application/javascript',
345 | 'content-encoding': 'gzip',
346 | 'vary': 'Accept-Encoding'
347 | }
348 | });
349 | pushed.on('error', err => console.log('HTTP server: push error ', err));
350 | pushed.end(cache[file]);
351 | });
352 | }
353 | response.writeHead(200, {
354 | 'content-type': url.endsWith('.js') ? 'application/javascript' : 'text/html',
355 | 'content-encoding' : 'gzip',
356 | 'vary': 'Accept-Encoding'
357 | });
358 | response.end(cache[url]);
359 | } else {
360 | response.writeHead(404);
361 | response.end();
362 | }
363 | }
364 |
365 | // Build task for serving generated builds over HTTP. Should be called after a
366 | // successful build.
367 | // Takes the following optional command line parameters:
368 | // * --http1: Serve over HTTP/1.1 instead of HTTP/2.
369 | // * --push: Use HTTP/2 push to push dependencies with the JS entry point.
370 | // * --preload: Add to HTML for all JS dependencies.
371 | gulpTask('serve', () => {
372 | const opts = {
373 | key: fs.readFileSync(path.join(__dirname, 'key.pem')),
374 | cert: fs.readFileSync(path.join(__dirname, 'cert.pem')),
375 | };
376 |
377 | if (process.argv.find(item => item === '--http1')) {
378 | http1 = true;
379 | }
380 | if (process.argv.find(item => item === '--preload')) {
381 | preload = true;
382 | }
383 | if (process.argv.find(item => item === '--push')) {
384 | push = true;
385 | }
386 |
387 | if (http1) {
388 | console.log('HTTP server: running in HTTP 1 mode.');
389 | opts['spdy'] = { protocols: ['http/1.1', 'http/1.0'] };
390 | } else {
391 | console.log('HTTP server: running in HTTP 2 mode.');
392 | opts['spdy'] = { protocols: ['h2', 'http/1.1', 'http/1.0'] };
393 | }
394 |
395 | if (push && !http1) {
396 | console.log('HTTP server: using HTTP 2 push.');
397 | } else if (push && http1) {
398 | console.log('HTTP server: no push support on HTTP 1.');
399 | }
400 |
401 | if (preload) {
402 | console.log('HTTP server: using .');
403 | }
404 |
405 | _cacheEverything();
406 |
407 | server = spdy.createServer(opts, _onRequest);
408 | console.log('HTTP server: listening...')
409 | server.listen(44333);
410 | });
411 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "module-loading-benchmarks",
3 | "version": "0.1.0",
4 | "description": "Performance measurement for ECMAScript browser module loading.",
5 | "private": true,
6 | "license": "Apache-2.0",
7 | "author": "Google",
8 | "scripts": {
9 | "build": "gulp build",
10 | "serve": "gulp serve",
11 | "bundle": "./gen-bundles.sh",
12 | "postinstall": "cd src/moment && npm install && cd ../three && npm install"
13 | },
14 | "engines": {
15 | "node": ">=12.0.0"
16 | },
17 | "dependencies": {
18 | "babel-core": "^6.24.1",
19 | "babili": "^0.1.4",
20 | "del": "^3.0.0",
21 | "merge-stream": "^1.0.1",
22 | "resolve-from": "^3.0.0",
23 | "rollup": "^0.42.0",
24 | "rollup-plugin-node-resolve": "^3.0.0",
25 | "spdy": "^3.4.7",
26 | "webpack": "^3.0.0",
27 | "webpack-stream": "^3.2.0"
28 | },
29 | "devDependencies": {
30 | "@babel/core": "^7.13.10",
31 | "babel-preset-minify": "^0.5.1",
32 | "gulp": "^4.0.2",
33 | "gulp-babel": "^8.0.0",
34 | "gulp-rename": "^2.0.0",
35 | "gulp-replace": "^1.0.0",
36 | "gulp-rollup-each": "^3.0.2"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Module loading benchmark sample.
4 | * Copyright 2017 Google Inc. All rights reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * https://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License
17 | *
18 | */
19 | package main
20 |
21 | import (
22 | "bytes"
23 | "compress/gzip"
24 | "encoding/json"
25 | "flag"
26 | "fmt"
27 | "html/template"
28 | "io"
29 | "io/ioutil"
30 | "log"
31 | "mime"
32 | "net/http"
33 | "net/url"
34 | "path"
35 | "regexp"
36 | "strconv"
37 | "strings"
38 | "time"
39 | )
40 |
41 | var (
42 | httpAddrFlag = flag.String("http", ":44333", "Listen address")
43 | preloadFlag = flag.Bool("preload", false, "Add to HTML for all JS dependencies")
44 | modulePreloadFlag = flag.Bool("modulepreload", false, "Add to HTML for all JS dependencies")
45 | pushFlag = flag.Bool("push", false, "Use HTTP/2 push to push dependencies with the JS entry point")
46 | http1Flag = flag.Bool("http1", false, "Serve over HTTP/1.1 instead of HTTP/2")
47 | gzipFlag = flag.Bool("gzip", false, "Use Content-Encoding: gzip")
48 | )
49 |
50 | var cache = map[string][]byte{}
51 | var pushFiles = map[string][]string{}
52 | var synthesizedTemplate *template.Template
53 |
54 | func main() {
55 | flag.Parse()
56 |
57 | // Load all served content into memory.
58 | err := cacheEverything()
59 | if err != nil {
60 | fmt.Println("error:", err)
61 | return
62 | }
63 |
64 | http.HandleFunc("/", onRequest)
65 | http.HandleFunc("/synthesized/", handleSynthesized)
66 | http.HandleFunc("/synthesized/a.js", handleJs)
67 |
68 | if *http1Flag {
69 | fmt.Printf("Server running at http://localhost%v\n", *httpAddrFlag)
70 | log.Fatal(http.ListenAndServe(*httpAddrFlag, nil))
71 | } else {
72 | fmt.Printf("Server running at https://localhost%v\n", *httpAddrFlag)
73 | log.Fatal(http.ListenAndServeTLS(*httpAddrFlag, "cert.pem", "key.pem", nil))
74 | }
75 | }
76 |
77 | func cacheEverything() error {
78 | jsonBytes, err := ioutil.ReadFile(path.Join("dist", "filelist.json"))
79 | if err != nil {
80 | return err
81 | }
82 |
83 | var filelist map[string]interface{}
84 | err = json.Unmarshal(jsonBytes, &filelist)
85 | if err != nil {
86 | return err
87 | }
88 | for project, v := range filelist {
89 | jsfiles := v.([]interface{})
90 | unbundledHtml := path.Join(project, "unbundled.html")
91 | unbundledJs := path.Join(project, "unbundled", "app.js")
92 | unbundledHtmlContent, err := ioutil.ReadFile(path.Join("dist", unbundledHtml))
93 |
94 | if *preloadFlag {
95 | links := ""
96 | for i := range jsfiles {
97 | relative := path.Join(project, "unbundled", jsfiles[len(jsfiles)-1-i].(string))
98 | links += " \n"
99 | }
100 | unbundledHtmlContent = []byte(strings.Replace(string(unbundledHtmlContent), "", links+"", 1))
101 | }
102 | if *modulePreloadFlag {
103 | links := ""
104 | for i := range jsfiles {
105 | relative := path.Join(project, "unbundled", jsfiles[len(jsfiles)-1-i].(string))
106 | links += " \n"
107 | }
108 | unbundledHtmlContent = []byte(strings.Replace(string(unbundledHtmlContent), "", links+"", 1))
109 | }
110 |
111 | cache[unbundledHtml], err = encodeContent(unbundledHtmlContent)
112 | if err != nil {
113 | return err
114 | }
115 |
116 | for i := range jsfiles {
117 | file := jsfiles[len(jsfiles)-1-i] // Iterate in reverse order
118 | relative := path.Join(project, "unbundled", file.(string))
119 | cache[relative], err = readFileAndEncode(path.Join("dist", relative))
120 | if err != nil {
121 | return err
122 | }
123 | if relative != unbundledJs {
124 | pushFiles[unbundledJs] = append(pushFiles[unbundledJs], relative)
125 | }
126 | }
127 |
128 | for _, c := range []string{"bundled-unoptimized", "bundled-optimized"} {
129 | html := path.Join(project, c+".html")
130 | js := path.Join(project, c, "app.js")
131 | cache[html], err = readFileAndEncode(path.Join("dist", html))
132 | if err != nil {
133 | return err
134 | }
135 | cache[js], err = readFileAndEncode(path.Join("dist", js))
136 | if err != nil {
137 | return err
138 | }
139 | }
140 | }
141 | cache["index.html"], err = readFileAndEncode(path.Join("dist", "index.html"))
142 | cache["display-results.js"], err = readFileAndEncode(path.Join("dist", "display-results.js"))
143 | if err != nil {
144 | return err
145 | }
146 |
147 | synthesizedTemplate = template.Must(template.ParseFiles("server/template/synthesized.html"))
148 | return nil
149 | }
150 |
151 | func readFileAndEncode(path string) ([]byte, error) {
152 | content, err := ioutil.ReadFile(path)
153 | if err != nil {
154 | return nil, err
155 | }
156 | return encodeContent(content)
157 | }
158 |
159 | func encodeContent(content []byte) ([]byte, error) {
160 | if !*gzipFlag {
161 | return content, nil
162 | }
163 | var buf bytes.Buffer
164 | gz := gzip.NewWriter(&buf)
165 | _, err := gz.Write(content)
166 | gz.Close()
167 | return buf.Bytes(), err
168 | }
169 |
170 | var randomizedPrefixRegexp = regexp.MustCompile(`^/(r/\d+/)?`)
171 |
172 | func onRequest(w http.ResponseWriter, r *http.Request) {
173 | upath := r.URL.Path
174 | if upath[len(upath)-1] == '/' {
175 | upath += "index.html"
176 | }
177 | upath = randomizedPrefixRegexp.ReplaceAllLiteralString(upath, "")
178 |
179 | content, ok := cache[upath]
180 | if !ok {
181 | http.ServeFile(w, r, path.Join("dist", r.URL.Path))
182 | return
183 | }
184 |
185 | pushIfPossible(w, r, upath)
186 |
187 | ctype := mime.TypeByExtension(path.Ext(upath))
188 | if ctype != "" {
189 | w.Header().Set("Content-type", ctype)
190 | }
191 | if *gzipFlag {
192 | w.Header().Set("Content-Encoding", "gzip")
193 | } else if strings.HasPrefix(r.URL.Path, "/r/") {
194 | // Add randomized comment so that it won't hit content-based cache.
195 | io.WriteString(w, "// "+r.URL.Path+"\n")
196 | }
197 | w.Write(content)
198 | }
199 |
200 | func pushIfPossible(w http.ResponseWriter, r *http.Request, path string) {
201 | if !*pushFlag {
202 | return
203 | }
204 | pushes, ok := pushFiles[path]
205 | if !ok {
206 | return
207 | }
208 | pusher, ok := w.(http.Pusher)
209 | if !ok {
210 | return
211 | }
212 | options := &http.PushOptions{
213 | Header: http.Header{
214 | "Accept-Encoding": r.Header["Accept-Encoding"],
215 | },
216 | }
217 | for _, file := range pushes {
218 | if err := pusher.Push("/"+file, options); err != nil {
219 | fmt.Printf("Failed to push %s: %v\n", file, err)
220 | }
221 | }
222 | }
223 |
224 | func handleSynthesized(w http.ResponseWriter, r *http.Request) {
225 | query, err := url.ParseQuery(r.URL.RawQuery)
226 | if err != nil {
227 | http.Error(w, err.Error(), http.StatusInternalServerError)
228 | return
229 | }
230 |
231 | // TODO: Support -preload and -push in synthesized tests
232 |
233 | scriptUrl := "a.js"
234 |
235 | if len(query["depth"]) > 0 {
236 | scriptUrl += "?depth=" + query["depth"][0]
237 | } else {
238 | scriptUrl += "?depth=5"
239 | }
240 |
241 | if len(query["branch"]) > 0 {
242 | scriptUrl += "&branch=" + query["branch"][0]
243 | }
244 |
245 | if len(query["cacheable"]) > 0 {
246 | scriptUrl += "&cacheable"
247 | }
248 |
249 | if len(query["delay"]) > 0 {
250 | scriptUrl += "&delay=" + query["delay"][0]
251 | }
252 |
253 | w.Header().Set("Content-Type", "text/html")
254 |
255 | synthesizedTemplate.Execute(w, map[string]string{"ScriptUrl": scriptUrl})
256 | }
257 |
258 | // Query parameters:
259 | // cacheable (optional) - add Cache-Control: max-age=86400
260 | // delay=n (optional) - sleep n milliseconds in response handler
261 | func handleJs(w http.ResponseWriter, r *http.Request) {
262 | const header = `
263 | // Bogus script
264 | (function() {
265 | function notActuallyCalled(arg) {
266 | return 'This string not actually used: ' + arg;
267 | }
268 | `
269 | const footer = `
270 | })();
271 | `
272 | query, err := url.ParseQuery(r.URL.RawQuery)
273 | if err != nil {
274 | http.Error(w, err.Error(), http.StatusInternalServerError)
275 | return
276 | }
277 |
278 | w.Header().Set("Content-Type", "application/javascript")
279 | if len(query["cacheable"]) > 0 {
280 | w.Header().Set("Cache-Control", "max-age=86400")
281 | }
282 |
283 | if len(query["depth"]) > 0 {
284 | depth, err := strconv.Atoi(query["depth"][0])
285 | if err != nil {
286 | http.Error(w, err.Error(), http.StatusInternalServerError)
287 | }
288 | if depth > 0 {
289 | query.Set("depth", strconv.Itoa(depth-1))
290 |
291 | branch := 2
292 | if len(query["branch"]) > 0 {
293 | branch, err = strconv.Atoi(query["branch"][0])
294 | if err != nil {
295 | http.Error(w, err.Error(), http.StatusInternalServerError)
296 | }
297 | }
298 |
299 | params := query.Encode()
300 | if branch == 1 {
301 | fmt.Fprintf(w, "import {} from './a.js?%v';\n", params)
302 | } else {
303 | for i := 0; i < branch; i++ {
304 | fmt.Fprintf(w, "import {} from './a.js?%v&n=%d';\n", params, i)
305 | }
306 | }
307 | }
308 | }
309 |
310 | fmt.Fprint(w, header)
311 |
312 | for i := 0; i < 10; i++ {
313 | // just 100 bytes
314 | fmt.Fprintf(w, `
315 | function fib%d(n) {
316 | if (n < 2)
317 | return 1;
318 | return fib%d(n-2) + fib%d(n-1);
319 | }
320 | `, i, i, i)
321 | }
322 | fmt.Fprint(w, footer)
323 |
324 | if len(query["delay"]) > 0 {
325 | delay, err := strconv.Atoi(query["delay"][0])
326 | if err != nil {
327 | http.Error(w, err.Error(), http.StatusInternalServerError)
328 | }
329 | time.Sleep(time.Duration(delay) * time.Millisecond)
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/server/template/synthesized.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/display-results.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Module loading benchmark sample.
4 | * Copyright 2017 Google Inc. All rights reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * https://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License
17 | *
18 | */
19 |
20 | window.onload = function() {
21 | let results = { onload: Math.round(performance.now()) };
22 | let timings = performance.getEntriesByType('resource').filter(
23 | rt => rt.name.indexOf('.js') >= 0);
24 | results.nmodule = timings.length;
25 | results.firstFetchStart = Math.round(Math.min.apply(null, timings.map(rt => rt.fetchStart)));
26 | results.lastResponseEnd = Math.round(Math.max.apply(null, timings.map(rt => rt.responseEnd)));
27 |
28 | const items = [['nmodule', 'Number of modules', ''],
29 | ['onload', 'Time to onload', ' ms'],
30 | ['firstFetchStart', "First module's fetchStart", ' ms'],
31 | ['lastResponseEnd', "Last module's responseEnd", ' ms']];
32 | let table = '
';
33 | for (let [name, title, unit] of items)
34 | table += `