こんにちは。フロントエンドエンジニアをしている岐部(@beryu)と申します。
WEBサービスのフロントエンド開発や、iOS用ネイティブアプリ開発を担当しています。

先日、弊社内で行われたTOTEC 2014というチューニンガソンのフロントエンドチューニング部門で優勝することが出来たので、その時の様子や私が行ったチューニングの内容をご紹介します!
(普段の業務で行っているチューニングというより、競技に特化したレポートになっていますのであしからず。。)


フロントエンドのチューニンガソンって、具体的に何を競うの?

課題となるウェブサイト(左記のスクリーンショットのページ)の描画をどれだけ高速にできるか、を競います。

チューニング対象は1ページのみで、画面遷移はありませんが画像のスライドショー機能が実装されています。
勝手にレイアウトを変更したり、機能や効果(アニメーション等)を変更したりするのはNGで、高速化しつつも性能以外のユーザー体験は維持することが求められます。


ここで「フロントエンドの性能測定には指標がたくさんあるはずだけど、どうやって優劣を競うんだろう?」と思われた方も多いのではないでしょうか?
今回のチューニンガソンでは、当日の競技開始直前にレギュレーションの説明が行われ、以下の8項目で評価される事が明示されました。
  • HTMLファイルサイズ
  • CSSファイルサイズ
  • JavaScriptファイルサイズ
  • 画像ファイルサイズ
  • リクエスト数
  • DOM要素数
  • JavaScriptのエラー有無(1つでもエラーがあればNG)
  • チューニング前後の表示差異(ImageMagickによる平均画素数比較で閾値を超えるとNG)
評価はWebKitベースのヘッドレスブラウザを使って行われ、社内で選ばれた100名弱のエンジニアでスコアを競います。
以上のルールで、11:00~18:00の7時間に渡って競技が行われました。

なお、エンジニアの順位は会場のプロジェクタやイントラネットでリアルタイムに公開される他、上位の順位は専用のTwitterアカウントでも実況されます。

作戦

発表されたレギュレーションを受けて、私は以下のような作戦で進めることにしました。

スコアに関係無さそうなチューニングは行わない

幸いなことに評価項目が明示されたので、行うべき施策の取捨選択が重要だと考えました。
例えば、「ブラウザのレンダリング負荷の軽減」や「アニメーションのfps改善」などは今回時間を費やすべきでないチューニングになりそうです。

一つ一つのチューニングに時間をかけない

性能改善策はいくつもある反面、作業時間は7時間しかないので、改善策をどれだけ多く適用できるかが勝敗を分ける事になりそうです。
時間との勝負なので、普段なら気にするような細部の実装も今回は放っておき、数をこなす事を意識しました。

チューニング開始

ここから、当日行ったチューニングの内容を時系列でご紹介します。
(時刻はgitのコミットログから予測したものなのでだいたいです)

11:00 初期状態のファイルを別名で保存

目的:チューニング後のデグレード確認環境の準備
チューニング対象のコードはgitリポジトリごと渡されたのでバージョン管理されているのですが、あえてローカルに別ファイルとしてコピーしておきました。
サクッとオリジナルのファイルが参照できる状態にしておけば、デグレードの確認が手軽に行えるので気持ち的にも安心です。

11:00 Gruntをインストール

目的:各種作業効率化ツールの利用
業務でGruntを使用していたので、このチューニンガソンでもGruntを使うことにしました。
(エンジニアさんによってはgulp.jsを使っている方も居らっしゃったみたいです。)

インストールしたGruntプラグインは以下の通りです。

プラグイン名目的
grunt-contrib-concat複数のJavaScriptファイルを1つのファイルに結合する
grunt-contrib-uglifyJavaScriptをminify(スペース・改行を除去、変数名や関数名を短縮)する
grunt-contrib-cssminCSSをminifyする
grunt-contrib-htmlminHTMLをminifyする
grunt-image画像ファイルを軽量化する
grunt-uncss使われていないCSSプロパティを取り除いたCSSファイルを生成する(後述)


11:10~ 課題ページの確認&PageSpeed Insightsの実行

目的:チューニング対象のウェブサイトの改善の余地を調査
上記のgruntプラグインをインストールする npm install コマンドを実行しながら、ブラウザやIDEでチューニング対象のウェブサイトを確認し始めました。
少し見ただけでもCSSの構文エラーがあったり、使っていないJavaScriptライブラリがインポートされていたり…。
まるで無茶な運用を数ヶ月続けたかのような、カオスなファイル群でした。

ここで実行した PageSpeed Insights に画像サイズの最適化をオススメされたので、まずはそこから行うことにしました。

11:20~ 画像ファイルの最適化

目的:画像ファイルサイズの削減
30 x 30pxで表示している画像ファイルが実際には150 x 150pxで保存されていたりする画像がそこそこあったので、それらのリサイズを行いました。
ここは画像毎にリサイズしたい解像度が異なっていたので地道にPhotoshopで縮小していったのですが、横幅を基準にリサイズした結果、高さが割り切れない数値になって1px狂ってしまったものがいくつかあり、それらが原因で評価項目の一つである
チューニング前後の表示差異(ImageMagickによる平均画素数比較で閾値を超えるとNG)
のチェックがNGになる問題が多数発生しました。
しょっぱなから解像度の微調整に結構な時間を食われてしまったので、悔いが残る工程となってしまいました…。

画像のリサイズとは別に、殆どの画像がPNGで保存されていたので、アルファチャンネルが必要無さそうな画像をPNG形式からJPG形式に変換する作業もここで一緒に行いました。

12:XX~ お昼ごはん

いつのまにか会場の後方でサンドイッチが配布されていました。
(ノイズキャンセリングイヤホンを付けて集中して作業していたので、昼食のアナウンスに気付けなかったのだと思われます…)

とは言え食べる時間も惜しいので、とりあえずサンドイッチは受け取るだけ受け取って作業の合間にちまちまと食べていく事にしました。

12:20~ jQueryのバージョンアップ

目的:JavaScriptファイルサイズの削減
ページ内でjQuery1.11.1が使用されていたのですが、WebKitブラウザでしかチェックしないレギュレーションだったのでjQuery2.1.1に変更しました。
jQuery2.X系は、jQuery1.X系から一部の古いブラウザ対応用の実装を取り除いたバージョンで、その分ファイル容量が軽くなっています。

本当はZepto.jsにしたかったんですが、jQueryからZepto.jsに置き換えるだけだとJavaScriptエラーが発生してしまっていたので、今回は諦めました。

12:25~ 不要そうなJavaScriptライブラリの削除

目的:JavaScriptファイルサイズの削減
たくさんのJavaScriptライブラリが読み込まれていたので、1行1行削除してみてJavaScriptエラーの発生しないものは取り除いていきました。

初期状態のscript要素(29行)

<script src="javascripts/jquery-1.11.1.js"></script>
<script src="javascripts/underscore.js"></script>
<script src="javascripts/underscore.string.js"></script><!--[if lte IE 6]>
<script src="javascripts/DD_belatedPNG_0.0.8a.js"></script>
<script>DD_belatedPNG.fix('body *');</script><![endif]-->
<script src="javascripts/backbone.js"></script>
<script src="javascripts/bootstrap.js"></script>
<script src="javascripts/coffee-script.js"></script>
<script src="javascripts/createjs-2013.12.12.min.js"></script>
<script src="javascripts/imgLiquid.js"></script>
<script src="javascripts/jquery-ui.js"></script>
<script src="javascripts/jquery.bxslider.js"></script>
<script src="javascripts/jquery.cycle.all.js"></script>
<script src="javascripts/jquery.ellipsis.js"></script>
<script src="javascripts/jquery.heightLine.js"></script>
<script src="javascripts/jquery.localscroll.js"></script>
<script src="javascripts/jquery.maximage.js"></script>
<script src="javascripts/jquery.scrollto.js"></script>
<script src="javascripts/jquery.slides.js"></script>
<script src="javascripts/jquery.smarttruncation.js"></script>
<script src="javascripts/masonry.pkgd.js"></script>
<script src="javascripts/paper-full.js"></script>
<script src="javascripts/tabulous.js"></script>
<script src="javascripts/trunk8.js"></script>
<script src="javascripts/jquery.smoothScroll.js"></script>
<script src="javascripts/modernizr.custom.86912.js"></script>
<script src="coffeescripts/templates.coffee" type="text/coffeescript"></script>
<script src="coffeescripts/jsons.coffee" type="text/coffeescript"></script>
<script src="coffeescripts/main.coffee" type="text/coffeescript"></script>

整理後のscript要素(20行)

<script src="lib/jquery-2.1.1.min.js"></script>
<script src="lib/underscore.js"></script>
<script src="lib/underscore.string.js"></script>
<script src="lib/backbone.js"></script>
<script src="lib/bootstrap.js"></script>
<script src="lib/coffee-script.js"></script>
<script src="lib/createjs-2013.12.12.min.js"></script>
<script src="lib/imgLiquid.js"></script>
<script src="lib/jquery.bxslider.js"></script>
<script src="lib/jquery.ellipsis.js"></script>
<script src="lib/jquery.heightLine.js"></script>
<script src="lib/jquery.localscroll.js"></script>
<script src="lib/jquery.maximage.js"></script>
<script src="lib/jquery.scrollto.js"></script>
<script src="lib/masonry.pkgd.js"></script>
<script src="lib/paper-full.js"></script>
<script src="lib/jquery.smoothScroll.js"></script>
<script src="coffeescripts/templates.coffee" type="text/coffeescript"></script>
<script src="coffeescripts/jsons.coffee" type="text/coffeescript"></script>
<script src="coffeescripts/main.coffee" type="text/coffeescript"></script>


12:40~ 無駄なCSSを削除

目的:CSSファイルサイズの削減
JavaScriptと同様、CSSも結構な数のファイルをロードしていて、相当な数が削減できることが予想できました。

初期状態のlink要素

<link rel="stylesheet" href="stylesheets/bootstrap-theme.css">
<link rel="stylesheet" href="stylesheets/bootstrap.css">
<link rel="stylesheet" href="stylesheets/jquery-ui.css">
<link rel="stylesheet" href="stylesheets/jquery-ui.structure.css">
<link rel="stylesheet" href="stylesheets/jquery-ui.theme.css">
<link rel="stylesheet" href="stylesheets/jquery.bxslider.css">
<link rel="stylesheet" href="stylesheets/jquery.maximage.css">
<link rel="stylesheet" href="stylesheets/normalize.css">
<link rel="stylesheet" href="stylesheets/tabulous.css">
<link rel="stylesheet" href="stylesheets/main.css">

この工程では11:00にインストールした grunt-uncss を使用してCSSを整理しました。
grunt-uncssは、特定のHTMLから使用されているCSSプロパティのみを抽出してくれるgruntプラグインです。


ちなみに、今回のチューニング対象ページは一部Ajaxで書き換えている箇所があったため、Chrome DevToolsのElementsタブからAjax通信後の状態のHTMLをコピーして別ファイルに保存してから実行しました。
※こうしないと、JavaScriptで後から描画するDOM要素で使用されているCSSプロパティが出力されません。

必要無いCSSプロパティを一気に省いたことでスコアもぐーんと伸び、この時点で初めてリアルタイムランキングで1位になりました。
この「ランキングがリアルタイムに見える」仕組み、チューニンガソンらしい臨場感があって楽しいです(常時他人と比較され続けるプレッシャーもかなりのものですが…)。

13:10~ CSSスプライト用の画像とCSSの生成

目的:HTTPリクエスト数の削減
CSSスプライト化が一切施されていなかったので、ページ内に含まれている画像をスプライト化する作業に入りました。
とはいえ、意地悪なことに提供されたファイル群には使用されていない画像ファイルも含まれています。
そこで、Chromeブラウザで開いたページを [ファイル] - [ページを別名で保存] から保存して、ページ内で使用されている画像ファイルだけを取り出しました。
ただ、この方法だとCSS内で背景画像に設定されている画像はローカルに保存されないので、それらだけはCSSファイル内を ".png" のようなキーワードで検索して手作業で保存しました。

スプライト画像とCSSはglueを使用して生成しました。

13:40~ CSSスプライトを適用しながらマークアップの掃除 Part.1

目的:HTTPリクエスト数の削減、DOM要素数の削減、HTMLファイルサイズの削減
上記作業の続きです。地味にこの工程が一番時間がかかったかもしれません。
ひたすらimg要素を削除し、親要素に上記のCSSスプライト画像を適用するクラスを付与していきます。
更に、無駄に深い入れ子構造になったマークアップが散見されたので、それらもどんどん削っていきました。

この辺りでそこそこ疲れてきたので、途中で止めていったん別の作業へ移ることにしました。

14:10 スプライト画像の減色

目的:画像ファイルサイズの削減
画像の圧縮は grunt-image を使用して行いました。
PNG画像の場合は少々画質が落ちてしまいますが、ファイルサイズが半分以下になる場合も多いので積極的に使います(普段の業務でも)。

14:20 JPG画像の画質向上

目的:スコア計測システムによるチューニング前後の表示差異検出回避
PNG画像の画質が荒くなったところで、評価項目の一つである
チューニング前後の表示差異(ImageMagickによる平均画素数比較で閾値を超えるとNG)
でFailになるようになってしまいました。
差分ファイルを見ると、特にJPG画像の圧縮ノイズが差分としてたくさん検出されてしまっていました。

そこで、11時台に画像ファイルの最適化を行った時に保存してあったpsdファイルから、再度画質をあげてJPGファイルを保存しなおしました。
念のためpsdファイルを取っておいて良かった…!

14:50~ CSSスプライトを適用しながらマークアップの掃除 Part.2

13:40の作業の続きです。
なんとかCSSスプライト化とDOM要素の削減がほぼ完了しました。

16:30~ 無駄なJavaScript実装を削除

目的:JavaScriptのファイルサイズの削減
CreateJSなど、アニメーションライブラリをロードするscript要素がHTMLに含まれていたのですが、ページ内には特に凝ったアニメーション演出は見当たりませんでした。
しかし、このライブラリのscript要素を削除するとJavaScriptエラーが起きてしまったので、アニメーションライブラリを使用した何かしらの実装が入っているようでした。

ページ内で読み込まれているJavaScriptファイルを参照すると、案の定canvas要素の初期化コードが入っているのを見つけました。
「画面内にcanvas要素が存在しなければこれらのコードは必要ない可能性が高い」と考え、Chrome DevtoolsのConsoleタブから下記のJavaScriptを実行して確認しました。

実行したJavaScript

document.getElementsByTagName('canvas');

結果

[]

上記のJavaScriptは、DOMツリーにcanvas要素が存在するか確かめるためのコードです。
結果が`[]`(=空っぽの配列)だったことから、今開いている画面にcanvas要素は一つも存在しないことが確認出来ました。
この結果からcanvas要素の初期化コードは無駄な実装だと判断し、該当箇所の実装をぱっと見で判断できる範囲で取り除きました。
厳密にはcanvas要素自体を追加/削除する実装が入っている可能性もゼロでは無いですが、そこまで実装を追うと時間がかかり過ぎるため確認は上記の内容程度にし、あとは「JavaScriptコードを削除した後にざっくりと動作確認する」という手法で進めました。

また、CoffeeScriptファイル(*.coffee)もJavaScriptファイル(*.js)に変換し、それに伴って不要になったcoffee-script.jsをロードするscript要素も削除しました。

17:20~ CSSの圧縮

目的:CSSのファイルサイズの削減
11:10に見つけていたCSSの構文エラーを修正してから、grunt-contrib-cssminを使用してcssを圧縮します。
HTML内のlink要素も圧縮版をロードするように書き換えました。

17:25~ JavaScriptの結合・圧縮

目的:JavaScriptのファイルサイズの削減、HTTPリクエスト数の削減
JavaScriptファイルの結合にはgrunt-contrib-concatを、圧縮にはgrunt-contrib-uglifyを使用しました。
CSSと同様に、HTML内のscript要素も圧縮版をロードするように書き換えました。

17:30~ JavaScript、CSSをインライン化

目的:HTTPリクエスト数の削減
今回のレギュレーションはキャッシュも考慮する必要がないので、JavaScriptファイルとCSSファイルを別ファイルとして保存するのではなく、HTMLファイル内にソースを丸ごと埋め込む施策を実施してみました。
これがうまくいけば、JavaScriptとCSSの2ファイル分のHTTPリクエストが削減できることになります。

しかしこれは暗黙のルール違反だったようで、スコア判定システム側でFailになってしまったので元に戻しました…。残念。

17:30~ 全画像の圧縮

目的:画像ファイルサイズの削減
スプライト画像だけはgrunt-imageで圧縮したものの、他のほとんどの画像はそのままだったので、このタイミングで全画像に対してgrunt-imageで圧縮処理を動かし始めました。
20分程度で全画像の圧縮が終ったので、その後JPGファイルだけImageOptimも通してからコミット。

17:35~ HTMLの圧縮

目的:HTMLファイルサイズの削減
HTMLファイルを圧縮するとチューニングしにくくなるのでこの時間まで避けていたのですが、そろそろ残り時間もなくなってきたのでgrunt-contrib-htmlminを使用してHTMLファイルを圧縮しました。
念のため、残りの時間は本体のコードを触らない事にします。

17:40~ .htaccess追加

目的:JavaScriptファイルサイズの削減、CSSファイルサイズの削減、HTMLファイルサイズの削減
たぶん無理だろうなあと思いつつも、試しにgzip圧縮を有効にする設定を記述した .htaccess をコミットしてスコアに反映されるか試してみました。
結果、やっぱり反映されませんでした。。

17:55~ 昼食の残りを食べる

やっとゆっくり出来る状況になったので、お昼に頂いたサンドイッチの残りを食べながら終了時刻を待ちました。

18:00~ 審査タイム

他の参加者のエンジニアさん達と、どんなチューニングをしたか等を雑談しながら結果発表を待ちます。

19:00~ 結果発表

優勝してた!

勝因は何だったのか

フロントエンドのチューニング技術は社内外の優秀なエンジニアが積極的に展開してくださっているので、恐らくどのプロジェクトもある程度似た事をされているのではないかと思います。

そんな状況下で勝負するには「何よりも作業スピードが肝心」と考え、割り切って次々に施策を実装する動き方が出来たのが良かったのかなぁと思っています。

最後に

この日に行った主なチューニングをまとめておきます。
  • 使用していないJavaScriptライブラリの削除
  • 使用していないCSSを削除
  • 無駄なDOM要素の削除
  • 無駄なJavaScript実装の削除
  • CSSスプライト化
  • HTML・JavaScript・CSSの結合およびminify
  • 画像解像度、容量の最適化
上記のチューニングを行った前後の数値は以下のようになりました。
チェック項目オリジナルチューニング後
HTMLファイルサイズ10547 バイト2688 バイト
CSSファイルサイズ292780 バイト22183 バイト
JavaScriptファイルサイズ1952540 バイト229505 バイト
画像ファイルサイズ12235961 バイト904482 バイト
リクエスト数14747
DOM要素数578449

自分の知識の範囲でやるべきと考えたチューニングを取捨選択して優勝できたので、とても嬉しい結果でした。
優勝賞金は、個人的な研究用に来年発売されるApple Watchを購入する資金にしようと思います!

最後までお読み頂き、ありがとうございました。