Posted at

Webパフォーマンス虎の巻

Webパフォーマンス向上施策のために、今更ながら超速本1を読んだので、今までの自分の知見と合わせてまとめてみます。

なるべく柔らかく、改善施策ってまず何をどうすればいいの?という疑問を持った人に向けて書いています。


▪️格言


そもそもWebは速い。遅くしているのは我々です。大抵は技術の問題ではなくて、人の問題。


引用元: テクニックではなく、今、本気で取り組むべきWebパフォーマンス (html5jパフォーマンス部 部長 竹洞さん)


心得

パフォーマンス向上に対する施策は大別すると以下の2通り



  • 軽量化 (単純にやりとりするデータ容量を小さくすること)


    • 圧縮

    • 削除




  • 最適化 (その時に最も適している実装・実行をとること)


    • 経路・順番の変更

    • 非同期



もっとも遅くしている原因を探して、それを対策するのが原則。「対効果」が絶対的正義である。手段から入るのは愚策。まず先に原因を知ることが重要。


▪️1. 計測・調査

パフォーマンス向上したいのに計測にお金をかけないのは、本心は向上しなくてもいいと思っている(!)。現状がどういった構造になっていて、どこがボトルネックになっているかを見つけることが最も重要なことです。


Chrome Developer Tools

Chromeに標準で備わっている開発者用ツールです。

今回は主に関連リソースの通信状況を閲覧できる「Network」タブと、ブラウザがレンダリングするまでの処理を閲覧できる「Performance」タブを利用します。


Network

まずは一番シンプルに、ページの読み込み完了までの数値をチェックします。ここでサイトがなぜ遅いのかの傾向を知ります。


  • 青い縦線がDOMContentLoaded(HTMLのパースが終了してDOMが構築される)が完了したタイミング。

  • 赤い縦線がLoad(関連するリソースの取得・解析がすべて終了)が完了したタイミング。

遅延する主な原因
主な対処方法

DOMContentLoaded
HTMLが巨大であったり、扱うDOMノードが大量。
DOM構築をブロックするスクリプトが存在する。
コンテンツ量を減らしたりしてHTMLを軽量化する。
同期的に読み込まれるJSファイルや、ベタがきされているスクリプトを排除する。

Load
JSや画像などサブリソースが大量に存在する(またはレスポンスが遅い)。
実行されるスクリプト処理やレンダリング処理が遅い。
読み込むリソースの量・サイズ・経路などを見直す。
強引な実装や非効率な処理を見直す。


Waterfall/Timing

Waterfallではリソースがどのタイミング・順番で、どれくらいの時間をかけて取得されたかがわかります。

ここでは以下の様に明らかにネットワークを圧迫しているリソースを簡単に見つけることができます。

各ファイルを選択するとより詳細なデータを閲覧できます。

ここではファイルの取得にあたって、どの処理がボトルネックになっているかを確認できます。


判断の一例

↑(TTFB < ContentDownload

配信サーバは強力なのにファイルのダウンロードが遅い(この場合はファイル容量が無駄に大きかった)

↑(TTFB > ContentDownload

ダウンロードに問題は無さそうだが、サーバからのレスポンスの方が悪影響を与えている

リソース単位でページが遅くなっている原因を見つけるのに非常に有用なツールです。


Performance

主に実装上のボトルネックを見つけるために使えます。計測すると、概要パネルへ時系列にグラフが描画されます。


FPS

緑色が高いほど高FPSがでていて安定して動作できていることを示しています。時間がかかっているフレームには赤いラインが上部に出て、スタックトレース欄で問題が確認できます。

選択すると下部に詳細が表示されます。

ここから具体的な原因になっている処理を特定することで、より負荷のかからない実装方法を検討する糸口になります。

また、Escキーを押すと出現するドロワー内に表示できるRenderingタブから、FPS meterを有効にすることでリアルタイムにFPSやGPU使用率の変動を確認できます。


CPU


  • 青はHTML関連

  • 黄はJavaScript関連

  • 紫はCSS関連

  • 緑はメディア(画像など)関連

  • 灰はその他

全体的に同じ色で埋め尽くされていれば、その領域の改善でCPU負荷を軽減できる可能性大。


HEAP/Memory

(Memoryにチェックを入れて計測すると、MemoryパネルとHEAPも同時に取得できます)



ここでメモリを圧迫している要素をつかむことができます。

Nodesが多い場合はDOMノードを見直したり、Listernersが増え続けている場合は不要になったイベントリスナは削除するなど試すといいかもしれません。

JS Heapはメモリリークが起きない様な実装を目指すことになるので、もっと詳しい記事を参照したほうがいいですスイマセン…


PageSpeed Insights

https://developers.google.com/speed/pagespeed/insights/?hl=ja

URLを入力するだけで、モバイル・パソコンの二種類の環境における最適化具合を診断してくれます。

直接改善できる可能性のあるファイルパスを教えてくれるため、最適化する対象ファイルを見つける手助けになります。

ただし、速度改善において非常に重要なネットワークに関連した評価はしてくれないので、PageSpeed InsightsでFastと診断されても本当に速いWebサイトとは限らないので鵜呑みにはしないほうが良さそう。

また、PageSpeed InsightsはCLIで実行できるnpmパッケージがあるので、自動化処理に組み込んで定期集計することも容易です。

https://www.npmjs.com/package/psi


WebPagetest

https://www.webpagetest.org/

実際に特定のリージョンからアクセスを実行し、ChromeDeveloperToolsのNetworkタブに相当するデータを取得できます。非常に詳細に計測結果がわかるので、Webサイトのパフォーマンスの現状を知るには良いツールでしょう。

コンテンツ
解析概要

Summary
計測を3回実行したサマリーと、Waterfallなどのキャプチャ

Details
ネットワークWaterfall、各リソース毎のネットワーク・リクエストタイミングの詳細

Performance Review
推奨される実装になっているかのチェック・スコア表示

Content Breakdown
MIME-type別のサブリソース詳細

Domains
ドメイン別のリクエスト数・リソース容量

Processing Breakdown
レンダリングまでの処理内訳

Screen Shot
レンダリング結果のキャプチャ画像

また、計測のためのAPIも提供されている。


RequestMap

Request Mapメニューを押すと外部サイトへ飛び、リクエスト処理の可視化がそのまま出来る。


SpeedCurve

https://speedcurve.com/

サーバーに何か実装を行うことなく、継続的に数値計測ができる有料サービス。(内部的には前述のWebPagetestが動いているので、結果は同じ)導入している企業も何社かお見かけしたことある。

以下のような強みがあるらしい。


  • 見やすいダッシュボード

  • 異なるデバイスなど、様々な環境から計測可

  • 競合サイトとのパフォーマンス比較

  • サードパーティスクリプトの解析

  • バジェット(閾値)の設定

  • API経由による外部ストレージへのデータ蓄積(→そこから別のダッシュボードサービスで可視化も可能)

デモ: https://speedcurve.com/demo/share/39tfnozeq94p1o0hndk1kpbg4vb7cg/


▪️2. ネットワーク

ページスピードを改善するためにはネットワーク経由で取得するリソースを最適化することになります。


  1. 必要最低限のリソースを

  2. 必要最小限のサイズで

  3. 最も適切なタイミングで

  4. 無駄なく

  5. ダウンロードすること。

そのため、ページの初期表示のためにどのリソースが本当に必要なのかを知っておくことが大事なポイントとなる。不要なものを後回しにするだけで、理論上ページロードは短くなる。


通信経路

現代のWebサイト/Webアプリは非常に多くのサブリソースを必要としているため、それらを効率よく取得することがパフォーマンス向上に直結している。まずはどのような経路でダウンロードされるかを整理する。


HTTP/2

HTTP/2にすると一つの接続において同時に複数のリクエスト/レスポンスをやりとりできる。

そのため特定のドメインから連続的に大量のリソースを取得するような場合は、リソース取得速度の向上が見込まれる。(HTTPヘッダの圧縮効率も高まるらしい)

ただし、伝送技術自体はHTTP/1.1と変わりないため変更しただけでスピードが上がるわけではない。

非同期な通信がしやすくなる点が大きいので、サブリソースの最適化を進める際の足場として非常に有用である。


CDN

Akamai2やFastly3などファイルの配信に最適化されたCDNサービスをうまく利用することで、大幅なページスピード向上が見込める。長期間変更されないファイルが点在する場合は、それらをCDNサービスのキャッシュサーバに配置することで、非常に高速にリソース取得ができる。(特に外部CDNサービスを使うメリットは、日本国外からのアクセスに対しても高速化しやすい点にある)

デプロイフローを簡略するために海外のWebサービスを使った場合(Herokuなど)、レイテンシを下げるためにCDNを使ってユーザーへコンテンツを配信する等が考えられる。

また、後述のキャッシュをどう運用していくかと密接に関係する。


Resource Hints

あらかじめ取得するリソースに関して、ある程度情報(URLやMIME-typeなど)を持っている場合はブラウザに対してヒントを与えてあげることで効率化できる(かもしれない)。

以下はそれぞれキーワード毎にもブラウザの実装状況はバラバラだが、非対応の場合は無視されるだけなのでプログレッシブエンハンスメント観点からも問題はあまりない。


DNS-prefetch

<link rel="dns-prefetch" href="//example.com">

異なるドメインからリソース取得することがわかっている場合、事前にDNSの名前解決を行う。DNSルックアップに時間がかかっている場合は効果があるかも。


preconnect

<link rel="preconnect" href="//example.com">

DNS-prefetchに加えて、TCPのコネクションまで貼ってくれる。


prefetch

<link rel="prefetch" href="/library.js" as="script">

リソースのURLがあらかじめ特定できている場合に有用。拡張子も判明するのでasプロパティで明示する。

ログイン画面→TOP画面のように次に遷移する画面が確定的なときに、必要なリソースファイルを事前取得できる。


prerender

<link rel="prerender" href="//example.com/next-page.html">

次のページを事前に全部レンダリングまでやっちゃう。遷移が非常に高速になるかもしれないが、高コスト。

(事前にレンダリングしてしまうので、レンダリング完了をフックにした処理がある場合などは注意。Page Visibility APIなどによる制御が必要になる模様。)


preload

<link rel="preload" href="sintel-short.mp4" as="video" type="video/mp4">

特定のリソースを経由して呼び出されるものは、元リソースがロードされないと判明しないため読み込みが遅くなることがある。(容量が大きいと尚更)

事前に呼び出しがわかっている場合は、別に該当リソースをロードすることができる。

また、CSSの非同期読み込みもpreloadで実行できるが、FOUC4が発生する可能性がある場合はクリティカルCSSと切り分けで実施したほうが良いでしょう。


軽量化

単純にサーバーとやりとりするデータ容量を削減する。みんな嬉しい。


HTML

どんなWebサイトでも、一番最初にHTMLをリクエストします。そのため、そのHTMLが速く・軽く提供されることは重要です。

余計な記述(コメントなど)が含まれている場合は削除したり、初期表示と関係のない要素(display: none;などCSSで最初は非表示にしてるコンテンツ等)は非同期に取得するような仕組みを検討しましょう。


API

ページロード後にユーザーの動作に応じてリクエストをするようなAPIサーバーの場合、ページ構築に最低限必要なデータのみをAPIとやりとりするようにして、少ないデータ量で実現すると良いかも。(API自体の設計にも関わるので、要検討)

一度に大量のデータをまとめて取得すると、リクエスト回数は最小限ですが、その後のスクリプト処理・レンダリング処理においてメモリリークを招く原因にもなりかねないので要注意。


gzip

コンテンツはgzipなど圧縮してから提供しましょう。


キャッシュ

キャッシュは高速化に大きく寄与できる機能なので、どのようなキャッシュ戦略にするかはよく検討した方がいいかもしれません。

基本的には以下のようなファイルを対象にしていきます。


  • ほとんど更新することがないファイル

  • 容量が巨大なファイル

  • アクセスするたびにダウンロードが必要なファイル

普段使っているJavaScriptファイルでも、変更されない領域を別にバンドルしてそっちをキャッシュさせるという戦略も取れます。(※Code Splitting)


ブラウザキャッシュ

配信するサーバー側にキャッシュの設定を追加します。


  • Cache-Control

  • Expires

ブラウザはリソースのHTTPヘッダを元に挙動を決定するので、サーバ側の設定によってブラウザキャッシュの動作をコントロールします。

参考: キャッシュについて整理


ServiceWorker + Cache



  • installイベントにおいて、Cacheインターフェースを使って特定のファイルをキャッシュする。


  • fetchイベントにおいて、キャッシュ済みファイルが既にある場合はそれを返す。
    ServiceWorkerなのでオフラインでも動作するのが強み。

大量・大容量のファイルをまとめてキャッシュしようとすると、install処理が大きくなるため注意。(確定的な対象ファイルを減らす等)

// installイベントでキャッシュ

self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/static/style.css',
'/static/app.js',
]);
})
);
});

// fetchイベントで該当した場合はキャッシュしたファイルを返す
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('v1').then(cache => {
return caches.match(event.request).then(res => {
return res || fetch(event.request.clone());
});
})
);
});

ServiceWorkerを使った実装はMDNに日本語の詳細なガイドがあるため、以下を参照のこと

https://developer.mozilla.org/ja/docs/Web/API/ServiceWorker_API/Using_Service_Workers


▪️3. メディアファイル

Webのリソースの多くを占めるのが画像を中心としたメディア系ファイルです。そのため、画像容量を削減することはWebサイトを軽くするための有効なアプローチになります。

ただし、どの画像にどの手法が効果的なのかは千差万別なので、特性を掴んでから取捨選択すると良いでしょう。


圧縮


最適な拡張子を選ぶ

画像圧縮の基本は拡張子です。どのような画像かによって、得意な圧縮形式は変わってくるためです。その中でも主に使用されていたり注目されているのが以下の4種類になります。

形式
拡張子
特徴

SVG
.svg
ベクターで表現できる場合に最適。
テキストデータとして表現されているためメタ情報を簡単に組み込める。

WebP
.webp
画質を綺麗に保ったまま、非常に軽量にすることができる新しめの形式。
2018年10月現在では対応環境が限られる。

PNG
.png
表現色数が豊かで透過処理にも対応している可逆圧縮形式。

JPEG
.jpg
写真やグラデーション表現などでも比較的軽量にすることができる不可逆圧縮形式。


圧縮率を上げる

画像はPhotoshopなどのレタッチソフトなどで圧縮率を設定して出力したあとからでも、まだ圧縮の余地は十分あることが多い。(※画像にもよります)

圧縮はアルゴリズムによって決まるので、より高圧縮できるライブラリが存在します。

便利に実行するためのツール類も充実しています。

また高解像度(Retinaディスプレイ等)向けの画像は、解像度が合っていれば圧縮率は結構高めでも人間の目にはあまり劣化して見えないとも言われています。


CSS Sprite

細か〜〜い画像が沢山あるような環境では、それを1本のリクエストに出来るので今でも有効な手段。

頻繁に更新がされなければ、ファイル数が少ないことはキャッシュ観点でも都合が良い。


出し分け


Media Queries

従来はPC/SPのレイアウト変更という印象が強かったが、画面サイズを元に適切なコンテンツを表示するという観点では無駄なリソースを省くような実装にすることで、パフォーマンス向上にも寄与できる。

/* 超速本内のサンプルコードより引用 */

.siteHeader {
background-image: url("image.jpg");
}

@media (max-width: 320px) {
.siteHeader {
background-image: url("image-320px.jpg");
}
}


srcset / picture

srcset属性やpicture要素は、ブラウザが自動的に閲覧環境に沿ったリソースをロードしてくれる。

適合しない場合はフォールバック指定しているものが読み込まれます。

<!-- srcset: 2x環境ではimage2x.jpgのみがロードされる -->

<img
srcset="image2x.jpg 2x"
src="image.jpg"
alt="image description"
>

<!-- picture: webpに対応していればそれを表示 -->
<picture>
<source srcset="image.webp" media="image/webp">
<img src="image.jpg" alt="image description">
</picture>


動的変換

リソース配信側サーバーを工夫して、imagingimageFluxのような動的に最適な画像を選択して表示できる仕組みも良いでしょう。

呼び出し側で拡張子・サイズ・画質などなどを決定できるので、継続した運用が必要なWebサービスなどでは効果が高いかも。

https://blog.cybozu.io/entry/2018/08/21/080000

https://knowledge.sakura.ad.jp/19287/


遅延読み込み

縦に長くて画像ファイルが大量にある場合は、ファーストビュー以外の画像を遅延読み込みするだけで初期描画はかなり改善する場合がある。(※そうでもないページの場合は、逆にユーザビリティを損ねる可能性があるので注意)

旧来はjQuery LazyLoadがよく使われていたが、ライブラリの容量が大きいためverlok/lazyloadPaul-Browne/lazyestload.js辺りが検討候補?

verlok/lazyloadのデモ:

https://www.andreaverlicchi.eu/lazyload/demos/with_srcset_lazy_sizes.html

(↑DeveloperToolsのNetworkタブを開いたままスクロールするとわかりやすい)


Blink LazyLoad

https://docs.google.com/document/d/1e8ZbVyUwgIkQMvJma3kKUDg8UUkLRRdANStqKuOIvHg/edit

現在Chromeが進めているLazyLoadをブラウザネイティブで実装しようという動き。期待半分くらいにウォッチしとくといいかも。


Webフォント最適化

Webフォントは綺麗な見た目をつくれるため便利なものですが、フォントファイルは容量が大きいため(特に日本語フォントはとてつもなくデカイ)、使用する際は注意が必要です。

フォント最適化について詳しく解説された記事↓

https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization?hl=ja


サブセット

同じフォントファイルでも、必要な文字だけに絞って軽量化することをサブセット化と言います。

ツールを使って不要な文字を抜いた軽量ファイルを生成することもできますし、GoogleFontsなどだと指定の仕方でサブセット化されたものをロードすることもできます。

<link href="https://fonts.googleapis.com/css?family=M+PLUS+1p&amp;subset=japanese" rel="stylesheet">


ローカル指定

@font-faceを使うことで、ユーザーのPCに該当のフォントが既にインストール済み場合それを代替的に使用するlocal()が利用できます。

@font-face {

font-family: 'MyFont';
font-style: normal;
font-weight: 400;
src: local('Noto Sans CJK JP Regular'),
local('NotoSansCJKjp-Regular'),
local('NotoSansJP-Regular'),
url('.../fonts/NotoSansCJKjp-Regular.woff2') format('woff2'),
url('.../fonts/NotoSansCJKjp-Regular.woff') format('woff'),
url('.../fonts/NotoSansCJKjp-Regular.ttf') format('truetype'),
url('.../fonts/NotoSansCJKjp-Regular.eot') format('embedded-opentype');
}

body {
font-family: MyFont;
}


Font Loading API

https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API

JavaScriptでフォントファイルのロードを制御できます。スクリプト処理に応じてフォントファイルが必要になる場合などは、こういったAPIを利用することで無駄なリソースを取得せずに済みます。

const font = new FontFace('Source Code Pro', 'url(source-code-pro.woff2)');

font.load().then(loadedFace => {
document.fonts.add(loadedFace);
document.body.style.fontFamily = `'Source Code Pro', monospace`;
});


▪️4. レンダリング

レンダリングのコストを削減すると以下の良いことが起きます。


  • ページの読み込み完了が速くなる

  • CPUの負荷が減る

  • カクついたりしなくなる

ユーザーがWebにアクセスする環境が多様化するなかでは、レンダリングが最適化されていることはユーザー体験に直結していきます。


async/deferによるレンダリングブロック回避

DOM構築をブロックする要素があると、いくら速くリソースを取得できたとしても画面に表示するのが遅れてしまいます。HTMLは上から順番に評価されるため、scriptは<head>要素内に記述された場合にそこでパースをストップしてscriptの実行を待ちます(ブロック)。

(レンダリングブロックについては右記URLが詳しいです: https://techblog.raccoon.ne.jp/archives/53180280.html

しかし、async/defer属性を付与されると次のような挙動に変化します。


async

async属性を付与されたscriptは、HTMLの処理とはまったく無関係に並行ロードされます。また、他のscriptの読み込み/実行を待ちません。

そのため、他scriptとの依存関係やDOMとは無関係な独立したscriptに適しています。

<script src="script.js" async></script>


defer

defer属性を付与されたscriptは、DOMContentLoadedの直前に、順番に実行されます。

レンダリングブロックを回避しつつ、HTMLのパース後に実行したいscriptに使用できます。

<script src="lib.js" defer></script>

<script src="my-script.js" defer></script>


レイアウト処理削減

画像ファイルは読み込み完了するまでそのサイズが確定できません。そのため、width/heightの記述が無い場合はレイアウト処理が何度も実行されてしまいます。

あらかじめ大きさがわかっている場合は明示的に記述しましょう。

<img src="image.jpg" alt="image description" width="200" height="150">

また画像の読み込み中は、プレースホルダーになるような矩形を用いてレイアウト処理を抑制するのも効果があるかもしれません。読み込み完了したら実際の画像と差し替えます。


イベント発火抑制

scrollresizeなど、一般的なWeb閲覧において何度も発生するようなイベントに対してレンダリングのコストがかかる処理が割り当てられている場合は、イベントが発火するたびに実行されてしまい、大きくパフォーマンスを落とす危険性があります。


イベントの発火を間引く

実際に紐づけられたイベントをユーザーに不便をかけない程度に間引くことで、パフォーマンスの劣化を小さくできます。

下記URLでは、lodashのthrottle()debounce()を使用して実践しています。

https://www.webprofessional.jp/throttle-scroll-events/


IntersectionObserver

IntersectionObserverのようなAPIを利用することで、指定要素がviewportに出現したタイミングを得ることできるため

scrollイベントのような劣化リスクの高い実装を避けることができることもあります。

let observer = new IntersectionObserver(callback, option);

observer.observe(target);


サーバーサイドレンダリング

モダンなライブラリなどに多い、scriptの実行によって大幅にコンテンツを生成したりする処理の場合、当然ながら複雑なスクリプト処理をサブリソース取得後に実行する必要があるため、コストが高く時間もかかります。

そこで、一番最初にコンテンツ生成済みのHTMLを返却してしまうのがサーバーサイドレンダリング(SSR)です。

サーバー側でプログラムを実行することになるので、TTFBに影響がでる恐れもありますが他数値と天秤にかけて判断できると思います。


掻い摘んでまとめているだけに過ぎないので、実践する際は各APIドキュメントや超速本のようなしっかりしたパフォーマンスに関する書籍を参考にしてから行なってみてください。





  1. https://webperf.guide/ 



  2. https://www.akamai.com/jp/ja/ 



  3. https://www.fastly.com/ 



  4. Flash of Unstyled Content」: CSSが適用されていないページが一瞬見えてしまうこと