✖
✖
先々週も先週も今週も、週末なぜかひたすら寝ていた。無限に疲れている。
サイトの最適化
HTTP2 化に伴なって、サイト全体の最適化を行ないました
依存の整理
もはや jQuery なしでも簡単に書けそうなスクリプト部分から jQuery 依存を抜きました。また、JSDeferred を Promise で置き換えました。
script 要素の async / defer
script 要素については必要に応じて async や defer をつけるようにし、基本的に外部スクリプトでブロックする可能性を排除しました。
async は script 要素同士で独立している場合無条件につけられます (非シーケンシャル)。defer はページのDOMが構築されたあとに実行されるように遅延されます (シーケンシャル)
defer は DOMContentLoaded 直前にまとめて呼ばれるようです。
外部ライブラリを自分でホスト
外部ライブラリをCDN経由でロードしている部分がありましたが、TLS セッションの無駄遣いな気がするので、自分でホストするように変えました。自分のホストであれば HTTP2 のセッションが生きているので無駄な処理が減りそうです。
動的圧縮から静的圧縮に
h2o で compress: ON とすると accept-encoding に応じて gzip か br で動的に圧縮してクライアントに返却してくれます。
しかし殆ど変更されないライブラリみたいなものは動的にやるのがもったいないので、静的に圧縮するようにしました。特に brotli は圧縮処理が遅い感じなので意味がありそうです。
h2o 的には file.send-compressed: ON にしって、filename.js.gz と filename.js.brを置くようにします。
.gz と .br をつくるスクリプト
以下のようなスクリプトを作っておくと、指定したファイルの .gz と .br を作れて便利です。
静的に作っておけば、特に VPS のスペックが非力だと相対的に効果が高いはずです。
#!/bin/sh
for f in "$@"
do
echo "$f"
gzip --best < "$f" > "$f".gz
bro --quality 10 --input "$f" --output "$f".br
done
brotli について
h2o は brotli に対応していて、accept-encoding: br なときはオンデマンドも br を返すみたいです。
ただ、brotli は chrome に実装済みといっても flags で有効にしないとまだ使えないみたいです。
Firefox 45 は標準で accept-encoding: br を吐くようです。
ブログシステムでのキャッシュ
今まで表示のたびに DB からひいてきてテンプレート処理を行っていましたが、キャッシュするようにしました。もともとそこまで遅いわけではないのですが 、一応これも効果がありました。
ただ、キャッシュを入れるとキャッシュ無効化の処理のために考えることが大変増えます。今のところあまり筋の良い実装ではないのですが、一応動いています。
MathJax のサーバサイド化
[tech] サーバーサイド MathJax で数式表示を高速化する | Fri, Apr 8. 2016 - 氾濫原 別エントリにしました。
いろいろやった結果
キャッシュなしの状態でロードして、DOMContentLoaded まで 300ms を切るぐらいにできました。ただし onload までは2秒〜3秒かかっています。onload まで遅いのは画像のサイズが大きいというのと、あとは主にアドセンスのせいです。
同じ環境で www.google.co.jp のDOMContentLoaded を測ると 170ms ぐらいでしたので、要素数などを比較するといい線まできている気がします。
さらにできることは?
いわゆる minify をやっていないので、これをやる余地がまだあります。しかし結構面倒です。そしてたとえ削れても数kBなので、意味があるのか疑問を持っています。
このエントリを参照するエントリ
サーバーサイド MathJax で数式表示を高速化する
このサイトでは数式を本文中に TeX 形式で書いて MathJax で処理させています。↓ こういうやつです。ベクターなので昨今の高解像度事情でもいい感じに綺麗に表示できます。
これはクライアントサイドで本文中の TeX フォーマットを探して全部 SVG とかに置き換えていくのですが、これがなかなか重い処理です。
特にスマートフォンの場合、数式がまともに表示されるまで5秒〜10秒ぐらいかかるので、それまでモヤモヤした感じになります。
さらにいえば、これは非同期で変換をかけているのでロード直後にリフローが頻発することになります。
ということで、これらの問題をなんとかしたいので、標題のようにサーバサイドで MathJax を使ってみるようにしてみました。
このサイトははてな記法で書いたエントリを保存時にHTMLに変換し、変換済みをDBにいれて、表示のときはそれを出しているだけです。なので、保存時に MathJax を通して本文中の数式を埋め込み SVG に変換するようにします。
HTML の断片をうけとって MathJax にかける HTTP サーバ
幸い MathJax-node という node.js 上で動かせる MathJax があったので、これをそのまま使い、http.Server でラップして、ブログシステムとは別に API サーバを立ち上げました。
#!/usr/bin/env node
var mjAPI = require("mathjax-node/lib/mj-page.js");
mjAPI.start();
mjAPI.config({
tex2jax: {
inlineMath: [["\\(","\\)"]],
displayMath: [ ["$$", "$$"] ]
},
extensions: ["tex2jax.js"]
});
var http = require('http');
http.createServer(function (req, res) {
var html = [];
req.on('readable', function () {
var chunk = req.read();
console.log('readable');
if (chunk) html.push(chunk.toString('utf8'));
});
req.on('end', function() {
console.log('end');
console.log('html', html);
mjAPI.typeset({
html: html.join(""),
renderer: "SVG",
inputs: ["TeX"],
ex: 6,
width: 40
}, function (result) {
console.log('typeset done');
console.log(result);
res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
res.end(result.html);
});
});
}).listen(13370, '127.0.0.1');
console.log('Server running at http://127.0.0.1:13370/');
ブログシステムからは保存時にこのAPIを呼んでHTMLをフィルタしてDBに保存するようにしました。
ハマりどころ
動的な MathJax だと、ページのコンテナサイズから適切な幅を計算してくれるのですが、サーバサイドだとそれができませんので、適切な横幅を自分で指定する必要がありました。
スマートフォンでの閲覧を考えると、width: 40 ぐらいにして、念のため CSS で svg { max-width: 100% } とかを入れておくとよさそうです。width が大きいと、数式ごとのフォントサイズが変わりまくって大変うっとおしいです。
MathJax-node の速度
MathJax-node はなぜかものすごく遅いです。ライブラリロード済みでも10秒ぐらいかかります。そもそも起動も遅いです。オンデマンドでやるのは無理そうなレベルです。
MathJax の JS のロードを抑制
これに伴なって、MathJax を使わなくても良さそうな場合、JSのロードすらやめるようにしました。
所感
MathJax の JS は2段階ぐらいでロードされるので RTT が長いほど不利になります。サーバサイドでやってしまえばJSを転送する必要も実行する必要もなくなるのでエコです。おかげでロードが結構早くなりました。
しかもページロード直後から数式が完全な形で表示されるので、ページに数式がある場合、体感的な速度は数倍に感じます。
このエントリを参照するエントリ
h2o のログ設定 (logrotate.d)
ログの設定を stdout のままにしていたので rotate するように設定しました。(Ubuntu 12.04.5 LTS)
h2o の設定
sudo mkdir /var/log/h2o sudo chown www-data:adm /var/log/h2o/
YAML の参照を使うとログフォーマットを使いまわせてよいです。まず global に
access-log: &ACCESSLOG
path: /dev/null
format: "..."
と書いて、あとは各ホストで path を上書きする形で設定します。
access-log:
<<: *ACCESSLOG
path: /var/log/h2o/host.access.log
なおこのYAML参照は h2o 2.0.0 からのサポートのようです。
logrotate の設定
# /etc/logrotate.d/h2o
/var/log/h2o/*.log { daily missingok rotate 90 compress delaycompress notifempty create 0640 www-data adm sharedscripts postrotate svc -h /service/h2o endscript }
h2o は daemontools で管理しているので、postrotate では svc を呼んで SIGHUP を送っています。
一旦状態を確認します
sudo logrotate -dv /etc/logrotate.d/h2o
全ログファイルが log does not need rotating になってるはずです。実際に一度実行します
sudo logrotate -v /etc/logrotate.d/h2o
/var/lib/logrotate/status を見ると該当するログファイルが記録されているはずです。日付を一日戻して再度実行するとローテートされることが確認できます。
このエントリを参照するエントリ
Let's encrypt の自動更新
以下のようなスクリプトを置いて月イチの cron で更新するようにしました。証明書を更新したあと h2o を restart しています。
#!/bin/sh
# sudo crontab -e
# MAILTO = cho45
# PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
# 25 14 6 * * /srv/www/renew-cert.sh
BIN=/home/cho45/project/letsencrypt/letsencrypt-auto
$BIN certonly --webroot -w /srv/www/www.lowreal.net --renew-by-default -d www.lowreal.net
$BIN certonly --webroot -w /srv/www/lowreal.net --renew-by-default -d lowreal.net
$BIN certonly --webroot -w /srv/www/cho45.stfuawsc.com --renew-by-default -d cho45.stfuawsc.com
svc -h /service/h2o
現在の h2o.conf.yaml
今のこのサイトの h2o.conf.yaml です。HTTPS (443) のみを処理しています。HTTP (80) は nginx で受けていて、HTTPS 対応ホストに関しては nginx からはリダイレクトしています。
- アクセスログフォーマットを LTSV に
- ログフォーマットを YAML の参照で全ホストで共有
- rewrite rule
- Strict-Transport-Security (HSTS)
- 一旦 https でアクセスしてきたクライアントに対して以後 http でのアクセスをさせない
- 本来はセキュリティのためだが、リダイレクトを一回減らせるのでパフォーマンス的にも一応得
- "/.well-known": をバインド
- letsencrypt のホスト検証に使われる
設定の際参考になれば幸いです。
user: www-data
access-log: &ACCESSLOG
path: /var/log/h2o/access.log
format: "time:%t\thost:%h\treq:%r\tstatus:%s\tsize:%b\treferer:%{Referer}i\tua:%{User-Agent}i\tcache:%{X-Cache}o\truntime:%{X-Runtime}o\tvhost:%{Host}i\tconnect-time:%{connect-time}x\trequest-header-time:%{request-header-time}x\trequest-body-time:%{request-body-time}x\tprocess-time:%{process-time}x\tresponse-time:%{response-time}x\tduration:%{duration}x\thttp2.stream-id:%{http2.stream-id}x\thttp2.priority:%{http2.priority.received}x"
error-log: /dev/stdout
http2-reprioritize-blocking-assets: ON
ssl-session-resumption:
mode: all
hosts:
"lowreal.net:443":
access-log:
<<: *ACCESSLOG
path: /var/log/h2o/lowreal.net.access.log
http2-casper: ON
compress: ON
listen:
port: 443
ssl:
certificate-file: /etc/letsencrypt/live/lowreal.net/fullchain.pem
key-file: /etc/letsencrypt/live/lowreal.net/privkey.pem
header.add: "Strict-Transport-Security: max-age=31536000"
header.add: "X-Content-Type-Options: nosniff"
header.add: "X-UA-Compatible: IE=Edge"
paths:
"/":
reproxy: ON
mruby.handler: |
require "/srv/www/rewrite_rules.rb"
lambda do |env|
RewriteRules.rewrite(env) do
rewrite '/favicon.ico', '/images/favicon.ico', :break
rewrite '/apple-touch-icon.png', '/images/apple-touch-icon.png', :break
rewrite %r{^/2005/colors-canvas\.xhtml$}, '/2005/colors-canvas.html', :permanent
rewrite %r{^/2005/colors-canvas$}, '/2005/colors-canvas.html', :permanent
rewrite %r{^/logs/latest$}, '/', :permanent
rewrite %r{^/logs/latest.rdf$}, '/feed', :permanent
rewrite %r{^/logs/latest.atom$}, '/feed', :permanent
rewrite %r{^/latest\.rdf$}, '/feed', :permanent
rewrite %r{^/blog/index\.(rdf|atom)$}, '/feed', :permanent
rewrite %r{^/logs(/.+?)(\.(rdf|atom))$}, '/feed', :permanent
rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
rewrite %r{^/blog(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
rewrite %r{^/photo$}, '/photo/', :permanent
rewrite %r{^/(\d\d\d\d/\d\d(/\d\d)?)$}, '/\1/', :permanent
rewrite %r{^/\d\d\d\d/$}, '/', :redirect
rewrite %r{^/view-img(/.+?)$}, '\1', :permanent
rewrite %r{^/(\d\d\d\d/([^\d]|\d\d[^/]).*)}, '/files/\1', :break
end
end
proxy.reverse.url: http://localhost:5001
proxy.preserve-host: ON
"/files":
file.dir: /srv/www/lowreal.net/files
"/images":
file.dir: /srv/www/lowreal.net/Nogag/static/images
"/css":
file.dir: /srv/www/lowreal.net/Nogag/static/css
"/js":
file.dir: /srv/www/lowreal.net/Nogag/static/js
"/lib":
file.dir: /srv/www/lowreal.net/Nogag/static/lib
"/.well-known":
file.dir: /srv/www/lowreal.net/.well-known
"www.lowreal.net:443":
access-log:
<<: *ACCESSLOG
path: /var/log/h2o/www.lowreal.net.access.log
http2-casper: ON
compress: ON
listen:
port: 443
ssl:
certificate-file: /etc/letsencrypt/live/www.lowreal.net/fullchain.pem
key-file: /etc/letsencrypt/live/www.lowreal.net/privkey.pem
header.add: "Strict-Transport-Security: max-age=31536000"
header.add: "X-Content-Type-Options: nosniff"
header.add: "X-UA-Compatible: IE=Edge"
paths:
"/":
reproxy: ON
mruby.handler: |
lambda do |env|
link = [
'/styles/201002/201002.css',
'/js/site-script.js',
].map{|p| "<#{p}>; rel=preload"}.join("\n")
case env['PATH_INFO']
when "/"
if (env['HTTP_ACCEPT_LANGUAGE'] || '') =~ /ja/
return [307, {"x-reproxy-url" => "/index.ja.html", "link" => link }, []]
else
return [307, {"x-reproxy-url" => "/index.en.html", "link" => link }, []]
end
when "/index.ja.html", "/index.en.html"
return [399, {"link" => link }, []]
end
return [399, {}, []]
end
file.dir: /srv/www/www.lowreal.net
# file.index: [ index.en.html ]
"cho45.stfuawsc.com:443":
access-log:
<<: *ACCESSLOG
path: /var/log/h2o/cho45.stfuawsc.com.access.log
listen:
port: 443
ssl:
certificate-file: /etc/letsencrypt/live/cho45.stfuawsc.com/fullchain.pem
key-file: /etc/letsencrypt/live/cho45.stfuawsc.com/privkey.pem
header.add: "Strict-Transport-Security: max-age=31536000"
header.add: "X-Content-Type-Options: nosniff"
header.add: "X-UA-Compatible: IE=Edge"
paths:
"/":
file.dir: /srv/www/cho45.stfuawsc.com
redirect:
status: 301
url: "/niro/"
"/niro/":
proxy.reverse.url: http://localhost:5001/niro/
proxy.preserve-host: ON
"/tmp":
mruby.handler: |
require "htpasswd.rb"
Htpasswd.new("/srv/www/.htpasswd", "Restricted")
file.dir: /srv/www/cho45.stfuawsc.com/tmp