この記事は2017/10/06のGotanda.js#9でLTした内容と同じです。
但しこちらでは、Node.js等のリリースと共に内容をアップデートしていく予定です。
(発表資料はsnapshotとしてアップデートしません)
(new) Node.js >= v10.0.0 HTTP/2 & ES Modules
core modulesにおいて、http2
をfeature flagなしで実行できるようになりました。
ES modulesについては未だフラグ付きなものの、dynamic importやimport.meta
がサポートされました。
koa
またはhapi
はhttp2サポートされているのでproductionで使い始めるところも出てきてるかもですが、10系のLTSは2018/10からなのでまだオススメしません。
express
においては未だhttp2サポートされてませんが、spdy
であれば可能なようです。
RisingStack: Node v10 is Here - Feature Breakdown!
(new) Performance by HTTP/2 + module
2017/10/07にChromeで大量のmoduleをロードした際のPerformanceについてのサマリーが公開された。
Loading Performance with (Many) Modules
これによると現状、HTTP/2でも下記が推奨らしい。
- 数百に細分化され、依存も深い(
depth>=5
)場合はバンドルすべき - 依存性の低いツリー(
depth<3
)で100未満のモジュール数であれば、パフォーマンス的に問題ない(ただしバンドルした方が高速)
すぐにパフォーマンス改善していくよ、と締められているが、もし今ES Modulesを実装するなら、dynamic importなど非同期読み込みを使った方が良さそうです。
What’s .mjs?
-
Node.jsでES Modulesを扱うための新拡張子
- Node.jsは
require()
などのModules APIを捨てる -
import/export
構文に統一 - flaggedで実行可能(>= v8.5)
- Node.jsは
node --experimental-modules hoge.mjs
- ブラウザではscriptタグ内で
type=module
で扱える
ブラウザ実装状況

before
<!-- browser -->
<script type=text/javascript src='bundle.js'></script>
// server
const pkg = require('pkg');
module.exports = {pkg};
after
<!-- browser -->
<script type=module src='not_bundle.js'></script>
// server
import hoge from './hoge';
export default hoge;
Diff (Node.js)
-
Modules APIが消えるのでそこから生えてたGlobal変数等も消える
module
require.cache
NODE_PATH
-
__dirname/__filename
-
import.meta
で代替
予定
-
- etc...
Diff (ECMAScript)
- not Function but Syntax
- 静的解析がしやすくなる
- 動的ロードはできなくなる
- not PATH but URL (Node.jsでは
file://
) - strict mode
- スコープが区切られる
-
this
はundefined
動的ロードェ...
const myPkg = env === 'development' ? require('../mypkg') : require('mypkg');
- 自分はNode.jsで環境によってpkg or directoryと読み込み方変えてた
- せめて
$NODE_PATH
が使えれば(´・ω・`) - 非同期でよければ
import()
(Dynamic Import)で代用可能
移行大変そう。でもこの一歩は、
Load to Universal JS
- ブラウザでも.mjsで読み込もう
<script type=module src="./foo.mjs">
- 但し
X-Content-Type-Options: nosniff
なのでContent-Type
をjsとして明示- Webサーバーによっては
Content-Type: application/javascript
で送ってくれない- 自分の環境ではapacheはダメだったけどexpressでは明示せずとも渡せた
-
application/octet-stream
はnosniff
だからダメ
- Webサーバーによっては
Try Universal JS
$cat validation.mjs
export default (input) => typeof input === 'string';
$cat browserside.js
<script type=module>
import validation from './validation.mjs';
if (validation('user-input')) console.log('ok'); // ok
</script>
$cat serverside.mjs && node --experimental-modules serverside.mjs
import validation from './validation';
if (validation('user-input from browser')) console.log('ok');
(node:72314) ExperimentalWarning: The ESM module loader is experimental.
ok
- validationをブラウザ・サーバー共通で使う例
- Node.jsではimportする側もES Modulesである必要がある
- ブラウザ側は拡張子まで含む必要があることに注意
Try Universal JS (using npm package)
$cat validation.mjs
import validation from './node_modules/myValidation/index.mjs';
export default (input) => validation(input);
- validationの中でnpm packageを使う場合の例
- nodeでは
from 'myValidation'
の指定で十分 - だがブラウザ側は拡張子まで必要
- なので現状だと上記のような工夫が必要......
HTTP/2
- not Text but Binary
- 1本のTCP接続の内部に仮想のTCPソケットを作成
- 並列接続でもハンドシェイク不要
- cf. HTTP/1.1では1 reqがsocketを占有した状態で並列接続(ex, Chrome:6本)
- Hpack
- headerの圧縮(頻出headerのkey/valを保持)
- Server Push
- CSS/JS/画像を事前に送りブラウザのキャッシュに入れておける
- 表示ページのassetだけでなく今後遷移するページのもPUSH可能(広告歓喜!)
Performance
- js/css/画像はなるべく少ないファイル数にすべき?
- それはHTTP1のTipsで、HTTP2では並列ロードにお任せしてESMで結合がベスト?
- でもbundleしたfileの方が総合的な通信量は減る
- でも分割した方が変更時のキャッシュヒット率が上がる
- 現実 (by 大津さん,ahomuさん)
HTTP/2 対応状況
- website全体の17.5%
- Nginx/Apache/Node.js(>= v8.4 (flagged))などで利用可能
- Nodeの実装はtatsuhiro_tさんのnghttp2ベース
- ちなみにNode.jsのweb serverは全体の0.3%
Try HTTP/2 with express
- 鍵作成
- 主要ブラウザにおいてHTTP/2はTLS前提
- 但しlocalhostはブラウザが特別扱いしてくれてTLS不要
- のはずがダメだった(ServiceWorkerでは不要だったのに)
-
http2
module
-
stream
イベントを利用するのが王道だがhttp/https
Compatibility APIを利用 - 従来通り
createServer()
にexpress()
を渡す
- 実行
- with
--expose-http2
flag
Try HTTP/2 with express: オレオレ証明書作成
$brew upgrade openssl # macデフォのだとversionが古くてできない
$openssl version
OpenSSL 1.0.2l 25 May 2017
$openssl genrsa -out my.key 2048
$openssl req -new -key my.key -out my.csr # 対話では全てdefaultを回答
$openssl x509 -req -days 365 -in my.csr -signkey my.key -out my.cert
Try HTTP/2 with express: http2 module
$cat server.js
const express = require('express');
const http2 = require('http2');
const fs = require('fs');
const options = {
key: fs.readFileSync(__dirname + '/my.key'),
cert: fs.readFileSync(__dirname + '/my.cert'),
};
const app = express();
app.use(express.static(__dirname + '/views'));
app.use(express.static(__dirname + '/mjs', {
setHeaders: (res, path, stat) => {
res.header('Content-Type', 'application/javascript');
}
}));
http2.createSecureServer(options, app).listen(8080);
Try HTTP/2 with express: 実行
$npm install express
$cat views/index.html
<h1>mjs</h1>
<script type=module src=./validation.mjs> <!-- mjs試すなら用意 -->
$node --expose-http2 server.js
(node:73026) ExperimentalWarning: The http2 module is an experimental API.
listenできた! https://localhost:8080/
を開くと......
_http_incoming.js:104
if (this.socket.readable)
^
TypeError: Cannot read property 'readable' of undefined
at IncomingMessage._read (_http_incoming.js:104:18)
at IncomingMessage.Readable.read (_stream_readable.js:445:10)
at IncomingMessage.read (_http_incoming.js:96:15)
at resume_ (_stream_readable.js:825:12)
at _combinedTickCallback (internal/process/next_tick.js:138:11)
at process._tickCallback (internal/process/next_tick.js:180:9)
対処法
-
https://github.com/nodejs/node/issues/14672
- まだexpressの内部実装が対応できてない様子
-
https://github.com/expressjs/express/pull/3390
- PRは出てるが、mergeされてない
- 実装は悪くなさそうなのでphouriさんのを使ってみる
Try HTTP/2 with express: 実行(revenge)
$git clone https://github.com/phouri/express.git -b initial-support-http2
$cat server.js
const express = require('./express'); // <- line.1を左記に変更
$node --expose-http2 server.js
https://localhost:8080/
を開くと......h2通信できた!

まとめ
- ES Modulesが来る!
- browserはほぼ実装済、Node.jsではこれから
- 移行大変そうだけど、Universal JSへの大事な一歩
- HTTP/2が来た!!
- 分割してもロードは早くなる(見込)
- Universal JSが、来る!!!
References
- .mjs
- HTTP/2