How to 速度改善 ー実装&技術調査編1ー

さて、[前回](https://qiita.com/masanarih0ri/items/1c3dc5ed33b0e61e8e58)までのところで原因はある程度調査することができました。

ここから、どういった実装をしていくかを考えていきたいと思います。

まずは「抱えている問題点」の整理をします。

## 抱えている問題点

1. 画像のサイズや大きさが適切でない
2. ページのレンダリングをブロックする要素が多い
3. キャッシュなどを設定していないのでHTTPリクエストの数が多い
4. ファーストビューに必要な情報以外の情報も多数読み込んでしまっている

というところに落ち着きそうです。

2.と4.については似たような話ではあるので、どちらかが解決されれば解決されている問題も含んでいそう、ということで

* 画像のサイズを適切なものにして、画像を圧縮する（可能であればその仕組みを作る）
* jsやcssの読み込みのタイミングを制御して、HTMLのパース、CSSでのレイアウトなどにかかる時間を減らす
* キャッシュの設定をちゃんとやる

といった対策を撮る方向で検討していきます。

### 画像のサイズを適切なものにして、画像を圧縮する

これについてはまず、そもそもの画像サイズが大きい問題があるので、指摘されている画像の容量とサイズをひたすら小さくしていきます。ただし、Retinaディスプレイのこともあるので表示しているサイズの倍の大きさは欲しいところ。

```html
<img src="hoge.jpg" width="100" height="100">
```
というHTMLでimgを表示しているところがあるとすれば、だいたいoriginの画像は200×200
くらいのサイズが必要そうです。

ちなみに、何気なくimgタグにwidthとheightを指定していますが、こちらの方がページの表示が早くなります。

ページのロード、そして表示をする際には
Roading→Scripting→Rendering→Paintingという流れで進んでいくのですが、Renderingの中でLayoutを計算するフェーズがあり、img要素は大きさを指定しない場合はそのままのサイズで表示される仕様になっています。そのため、imgタグにcssで幅や高さを指定している場合は

・imgは一旦元の大きさでLayoutされる
・画像が読み込まれると大きさがかわりに再度Layoutされる

という流れになるので、余計に1回Layoutが行われ、余分なレンダリングが発生してしまいます。100個画像があったら100回余計なLayoutが増えるということですね。バカにはできません。

さて、話を戻します。画像ファイルの大きさを適切にするためにはどうするか、ということです。

まずはシンプルに圧縮していってみましょう。

圧縮には
[ImageOptim](https://imageoptim.softonic.jp/mac)
[iLoveIMG](https://www.iloveimg.com/ja/compress-image/compress-png)
[compressjpeg](https://compressjpeg.com/ja/)
あたりを使ってみました。

全然圧縮してない時にはたくさんのファイルが指摘されていましたが、圧縮を頑張ってみた結果

![qiita4-1.png](https://qiita-image-store.s3.amazonaws.com/0/76779/b8b8c77f-3a74-2f57-0b9b-1efa7108401d.png)

3つだけの指摘になりました！:laughing:
最初の指摘のキャプチャを撮り忘れたので比較できないのがアレなんですが、15個くらい指摘されていて、ここだけで1sくらいの遅延が起きていました。

問題は一つ解決できたのですが、定期的にこの状況をチェックして、それぞれの画像を圧縮するという行為はなかなか辛いので

・[gulp](https://gulpjs.com/)などのタスクランナーを使って自動的に画像を圧縮する
・[CloudFront](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Introduction.html)などのCDN配信を使う

といったことを今後検討していく必要がありそうです。

gulpについては[このような記事](https://qiita.com/MikaShirahama/items/ab91624709510c496e53)を参考にして、CloudFrontについては[公式の情報](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/setting-up-cloudfront.html)を見ながら進めるのが良さそうですね。

できれば早い段階でやってみたい。。

### jsやcssの読み込みのタイミングを制御

さて、次にページをレンダリングをブロックしている要素についてみていきましょう。

今回調査した内容ですと
xxx.js
yyy.css

という感じでjsファイルとcssファイルがレンダリングをブロックする要素に挙げられています。
まずはjsのレンダリングブロックについてみていきましょう。

#### JSの実行について

Javascriptの実行タイミングは、基本的にはコンテンツ（HTML,CSS,Javascriptなど）をロードした後になります。
なので、Javascriptの読み込みの後に実行がされると、その間レンダリングの処理は走りません。CSSでのStyleの計算や、Layoutなどについてはその後にされることになります。
メインスレッド（UIスレッドともいう）はJavascriptに占拠されてしまうと、その先の処理が止まってしまいます。
極端な話、メインスレッドでJavascriptが動き続けていると、そのあとのレンダリングと描画がされない、ということになります。
今回はその部分をPage Speed Insightsで指摘されていました。

#### ではどうするか？

Javascriptの読み込みを非同期にしてみます。

```html
<script src="hogehoge.js"></script>
```
のように読み込んでいる箇所があるとすれば

```html

<script src="hogehoge.js" async></script>
```
とするか

```html

<script src="hogehoge.js" defer></script>
```
としてみましょう。

asyncとdeferはどちらもJavascriptの外部ファイルを非同期で読み込むことになりますが、若干挙動が違います。これによって、どちらを使うかが分かれます。

具体的には

**deferは上から順番に非同期処理をしてくれるが、asyncは非同期処理の順番が制御できない。**

という違いがあるため、下記のような場合ではasyncは使うべきではありません。

```html
<script src="jquery.js" async></script>
<script src="use_jquery.js" async></script>
<script src="use_jquery2.js" async></script>

<!-- ↑のどれが先に読み込まれるかわからないので、use_jquery.jsとかuse_jquery2.jsがjqueryを使っているとエラーが発生する可能性がある -->
```

読み込みの順番がランダムになってしまうと困る場合は、asyncではなくdeferを使いましょう。

私の場合も、jqueryを読み込んでいる箇所や、別のサーバーから配信されているjsなどがいくつかあったため（そしてそれが色々なページで使われているため）、asyncではなくdeferを使いました。

対象のアプリがRailsで動いているので

・jsファイルについては全てのファイルを1つにまとめている形になっている（Sprocketsとお別れしていない）
・layouts/application.html.erbの中にscriptタグが多数使われている。

という状況です。

ちなみにSprocketsを外して、Webpack入れようや〜、というのも簡単にはできなさそうなので、一旦そこは仕方なしとしました。

ということで、まずはdefer属性を全てのscriptタグにつけてみます。

```html
<script src="jquery.js" defer></script>
<script src="hogehoge.js" defer></script>
<script src="hogehoge2.js" defer></script>
<script src="hogehoge3.js" defer></script>
<script src="hogehoge4.js" defer></script>
<script src="hogehoge5.js" defer></script>
<script src="hogehoge6.js" defer></script>
```
RailsのメソッドでもJavascriptが読み込まれていたのでdefer属性をつけてみる

```erb
<%= javascript_include_tag "hogehoge", defer: 'defer' %>
<%= javascript_include_tag "fugafuga", defer: 'defer' %>
```

よし、これで万事解決！

と思いましたが、全然そんなことはなく...

```
Uncaught TypeError: $ is not a function
```
など色々なエラーが発生。。

まあ要するに、deferで全てのJavascriptを非同期に読み込んでしまうと、結局機能しない箇所が出てきてしまうみたいです。。。

うーむ:neutral_face:

ちなみにapplication.html.erbの中には直接

```html

<script type="text/javascript">window.$ = jQuery;</script>
<script type="text/javascript">jQuery.noConflict(true);</script>
```
のような記述があるため、その辺りも考慮しないといけないことがわかりました。

scriptタグの読み込みについて自分なりに調べてみたところ、以下の内容が判明しました。

**・何も属性を指定しない**
HTMLのパースを中断し、scriptを読み込む。そして実行する。
実行するまではHTMLのパースは再開されない

**・defer属性をつける**
非同期でHTMLパースと並行してダウンロードされるため、HTMLのパースを中断することはない。
scriptの実行はDOMが構築されてからで、DOMContentLoadedイベントのタイミングで実行される。


**・async属性をつける**
asyncもdeferと同じく非同期でHTMLパースと並行してダウンロードされるため、HTMLのパースを中断することはない。

ただ、ダウンロード完了後すぐにscriptが実行されるので、HTMLパースが完了する前にscriptが実行されてパースを中断することがある。

deferを使ってやる場合には、
**scriptの実行はDOMが構築されてからで、DOMContentLoadedイベントのタイミングで実行される。**
ということを意識して進めないとダメそうです。

さて、話を戻しますが、全部ではなく、いくつかのscriptタグ（Railsの方でコンパイルされているものとか）を対象から外してdefer属性を仕込んでみたところ、無事にエラーが消えていきました。

```html

<script type="text/javascript">window.$ = jQuery;</script>
<script type="text/javascript">jQuery.noConflict(true);</script>
```
となっている箇所については

```javascript

document.addEventListener("DOMContentLoaded", function(){
  window.$ = jQuery;
  jQuery.noConflict(true);
})
```
とするとエラーが解消されたので、その記述も追加しています。

これらの対応によって、多くのJavascriptファイルがレンダリングをブロックしなくなりました！！:smiley:

次にレンダリングをブロックしているCSSを減らす方法について調べてみたところ

・[HTMLにインラインでCSSを書く](http://webnonotes.com/web/speed/#sc1)
・[CDNでコンテンツを配信](https://aws.amazon.com/jp/cloudfront-cdn/)
・[preloadを使う](https://firstlayout.net/load-css-asynchronously-with-loadcss/)

といった方法がありました。

今回は、HTMLを1行書くだけで速度改善に繋がるpreloadを使ってみます。

ただ、preloadは下記の表のように、ChromeやSafariなどでは使えますが、IE, Edgeではまだ使えないようです。（なんなんMicrosoft・・・）

<img width="1166" alt="スクリーンショット 2019-03-13 15.50.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/76779/b12e16ae-2ff9-0804-b57a-a77f1294a57d.png">

[※Firefoxもバグがあったようでfirefox57以降では使えなくしたようです](https://developer.mozilla.org/ja/docs/Mozilla/Firefox/Releases/57#HTML_2)

ちなみにEdgeは

> MS Edge status: In Development
1 Only cachable resources can be preloaded. This includes the following as values: script, style, image, video, audio, track, fetch, and font (note font/collection is not supported).

> 2 Can be enabled via the "Experimental Features" developer menu

> 3 Disabled by default behind the network.preload flag.

> 4 Partial support in Edge 17+ refers to support for only the HTML <link rel> format, not the HTTP header format.

とあり、現在「開発中」のステータスになっています。

この文章をGoogle翻訳に突っ込んでみると

・styleはpreloadできる（ようになる）

・"Experimental Features"開発者メニューから有効にすることができる（ようになる）

・Edge 17以降では、HTTPヘッダー形式ではなく、HTMLの<link rel>形式のみサポートする

微妙だ・・・。

[MDNのページ](https://developer.mozilla.org/ja/docs/Web/HTML/Preloading_content#The_basics)をみると

<img width="801" alt="qiita.png" src="https://qiita-image-store.s3.amazonaws.com/0/76779/84b4a67a-8908-4ffd-9598-cc76396ec4da.png">


のようなサンプルが載っていたので教科書通りに記述してみます。

Firefoxとかは対応していないみたいだけど、これはpreloadの属性の記述のところを無視して普通の記述の方を読みにいってくれているのだろうか・・・？

[ここ](https://firstlayout.net/load-css-asynchronously-with-loadcss/)には

> 通常 <link rel="preload"> に対応していないブラウザでは、例え記述をしても CSS は非同期で読み込まれません。

と書いてあるので、普通にcssが読み込まれているということなのでしょう。

一応表示崩れなどなさそうなので、問題なさそう。。

ただ、preloadを全てのブラウザに対応させる上では[loadCSSというライブラリ](https://github.com/filamentgroup/loadCSS/blob/master/src/cssrelpreload.js)を入れる必要がありそうなので、IEやEdge、それとFirefoxにおいて表示速度をあげたい場合にはこれを入れるのが妥当な線なのでしょうね。

さて、これらの対応ができたので、状況を確認してみます。

Page Speed Insightsでスコアを測りたいのですが、それはQA（テスト）環境ではできないようなので、一旦デベロッパーツールのAuditsを使ってみてみましょう。

元がこれ
![qiita5-1.png](https://qiita-image-store.s3.amazonaws.com/0/76779/9dca46b0-f005-589c-5230-42f41a28f7e9.png)


修正後がこれ
![qiita5-3.png](https://qiita-image-store.s3.amazonaws.com/0/76779/539a0c08-71a4-8efb-c56d-1107bd175e52.png)

おお！！

ちゃんとレンダリングブロックの要素が減っている！
トータルの時間も3640ms→2610msに減っている！

よかったよかった。。

これで抱えている問題の1と２についてはある程度解消することができました。

キャッシュについては現在調査中なので、また詳細わかりましたら追記させていただきます。

また次回は、今回の修正で使わなかった速度改善に有効そうなテクニックやツール、そして計測方法などを共有する予定です。（他のページやアプリケーションでは使えるかもしれないので、お楽しみに！）