スピードハッカソンの内容を技術的に振り返る

今年も早いものでアドベントカレンダーの季節ですね。
勢いで12月序盤の記事投稿を予約したものの、全くネタを思いつかなかったので、
苦肉の策で以前に社内で行ったスピードハッカソンの技術的な振り返り記事を書くことにしました。

以前といっても１年以上前の話なのですが、、振り返りはしたいと思っていたのでちょうどいいはず。。

イベントの詳細はこちらの記事を御覧ください。
[ニジボックスのエンジニアがスピードハッカソン開催！「パフォーマンス改善で大事なのは、現状を知ること」 \| NIJIBOXのフロントエンドエンジニア](https://www.wantedly.com/companies/nijibox/post_articles/292188)

## Lighthouseでパフォーマンス計測
### [Lighthouse](https://github.com/GoogleChrome/lighthouse)とは
Webページの品質向上に役立つよう開発されたオープンソースのツールです。
以下の画像のようにGoogle ChromeのDevToolsのLighthouseタブを選択して、「Generate reportボタン」を押すだけでサイトのパフォーマンス、アクセシビリティ、ベストプラクティス、SEO、PWA の対応状況について確認することができます。
![lighthouse.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321338/7895a4e5-f669-e0b6-eb3f-c3f9413aa114.png)
Lighthouseを使用する際は、以下2点に注意してください。

- シークレットモードを使う
- Lighthouseの設定「Clear storage」をONにしておく

シークレットモードを使う理由は、Chromeの拡張機能がパフォーマンス計測に影響する可能性があるためです。（なので拡張機能をすべて無効にできた状態であれば通常のブラウザでも大丈夫です）

「Clear storage」はLighthouseタブ右上のsettingsボタンクリックから表示できるチェックボックスです。
デフォルトではONになっているので問題ないですが、OFFの状態だとブラウザキャッシュやLocal Storageなどの影響でスコアが高く出てしまうため注意してください。
![clearstorage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321338/aef138bc-24de-232c-25df-62c1ffe19b2c.png)



そして今回のスピードハッカソンでは、LighthouseのPerformanceのスコアをどこまで高められるかを競いました。

対応前のスコアがこちらで、
![before2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321338/af3c418d-fd64-89ea-c31f-87eed7b86f60.png)
以下記事の内容を対応後のスコアがこちらになります。
![after2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321338/d0133fd4-152a-9263-992b-414183133f56.png)

実際の競技内ではこんなにスコアを上げられなかったのですが、競技の振り返り解説の内容を踏まえてスコアを100点にできたので、その実装した内容を説明します。

## Image
まず始めに画像の容量が大きすぎる警告が出ていたため、画像の調整を行いました。

>**Properly size images**
>Serve images that are appropriately-sized to save cellular data and improve load time.


### 画像圧縮
[pngquant](https://github.com/kornelski/pngquant)は、CLI上でpng画像を圧縮できるツールです。
Homebrewなどでインストールした後に以下のコマンドで使用することができます。

```shell
$ brew install pngquant
$ pngquant --ext .png --force --speed 1 *.png
```

`--ext .png --force`は圧縮後の画像を元画像に上書きして生成するオプションです。
さらに`--speed 1`をつけることで圧縮効率最大で圧縮できます。

**参考** : [png画像の圧縮には pngquant を使おう [Mac/Win] - Qiita](https://qiita.com/thanks2music/items/309700a411652c00672a)


### Webpに変換
JPEGやPNGの画像を、さらに圧縮率の高い画像フォーマットWebPに変換します。

**参考** : [次世代画像形式のWebP、そしてAVIFへ。変わり続ける技術に対応するweb制作の黄金解 - ICS MEDIA](https://ics.media/entry/201001/)

```shell
$ brew install webp
$ for file in *.{jpg,png}; do cwebp $file -o ${file%.*}.webp; done
```

変換には[cwebp](https://developers.google.com/speed/webp/docs/cwebp)を使いました。
Linuxコマンドのfor文でループさせて、指定ディレクトリ内のすべてのjpgとpng拡張子画像をWebPフォーマットに変換しています。
変換後は画像読み込み記述のjpg、png拡張子もwebpに置き換えることを忘れずに。

**参考** : [Macコマンドで簡単！PNG＆JPGをWebPに一括変換 \| Cruw](https://cruw.co.jp/blog/webp-conversion/)

### 画像リサイズ
画像のサイズ自体が大きすぎる場合は、画像サイズを縮小する必要があります。
ここではCLI上で画像操作をするために[ImageMagick](https://imagemagick.biz/)を使用しました。

```shell
$ brew install imagemagick
$ convert image.webp -resize 50% image.webp
```
image.webpという名前の画像を大きさ50%にresizeしています。

### 遅延読み込み

`Defer offscreen images` の警告が出ている場合は、画像の遅延読み込み設定をしましょう。
設定は簡単で、imgタグのloading属性に`loading="lazy"`を記述するだけです。

```html
<img src="image.jpg" alt="..." loading="lazy">
```

**参考** : [Lazy loading - Web Performance \| MDN](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading)

## Font
Webフォントなどのフォントファイルは容量が大きく、パフォーマンス影響が出やすい部分です。
使用していないWebフォントのダウンロードをやめて、必要な分だけを読み込むようにしましょう。

### CSSファイルで読み込んでいる場合

例えばこのようにCSSファイルでAdobe Fontsを読み込んでいる場合、

```css:css
@import url("https://use.typekit.net/mqn2xnp.css");
```

読み込み先のurlにアクセスして、必要なfont-weightのみをimportするようにします。

```css:css
/* この行を削除 */
@import url("https://use.typekit.net/mqn2xnp.css");

/* 以下を追記 */
/* font-weight:700のフォントのみを読み込む */
@font-face {
  font-family:"soleil";
  src:url("https://use.typekit.net/af/1f781f/00000000000000003b9aef83/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3") format("woff2");
  font-display:auto;
  font-style:normal;
  font-weight:700;
}
```

対象ブラウザがChromeだけであれば読み込むフォントフォーマットはwoff2形式だけで大丈夫です。

**参考** : [WOFF (Web Open Font Format) - 開発者ガイド \| MDN](https://developer.mozilla.org/ja/docs/Web/Guide/WOFF)

さらに、Webフォントデータをローカルにダウンロードして自前サーバに配置することで、さらなるパフォーマンス向上も期待できます。その場合はフォントのライセンスを確認して、自由に使えるフォントかどうか注意してください。

```css:css
@font-face {
  font-family:"soleil";
  src:url("../font/soleil_700.woff2") format("woff2");  /* フォントをローカルに落としてきた */
  font-display:auto;
  font-style:normal;
  font-weight:700;
}
```

### HTMLから読み込む場合

例えばHTMLファイルからGoogle Fontsを読み込む場合、

```html:HTML
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
```

このように[preconnect](https://developer.mozilla.org/ja/docs/Web/HTML/Link_types/preconnect)キーワードをつけることで、指定したドメインへの接続（DNSルックアップ、TCPハンドシェイクもしくはTLSネゴシエーション）を事前に行うことができます。
preconnectはIEやFireFoxでは対応していないので、それらにも対応させたい場合は[dns-prefetch](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control)を使用してください。

**参考** : [Can I use preconnect](https://caniuse.com/link-rel-preconnect)

またcrossorigin属性をつけることも必須です。（[詳細](https://www.w3.org/TR/css-fonts-3/#font-fetching-requirements)）

### font-display: swapについて

Webフォントは外部からフォントデータをダウンロードする関係上、どうしても画面表示まで時間がかかります。
その場合、Webフォントに対して`font-display: swap`を指定することで、Webフォントがダウンロード完了するまでの間はシステムフォントでテキストを表示することができます。

```css:CSSの場合
@font-face {
  font-family:"soleil";
  src:url("../font/soleil_700.woff2") format("woff2");
  font-display:swap;  /* swapに変更 */
  font-style:normal;
  font-weight:700;
}
```
```html:HTMLの場合
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- &display=swapを追加 -->
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
```

フォントの切り替わりのタイミングで画面がちらつくので、より早くテキストを表示させたいときなどに使用してください。

以下Gif画像は`font-display: swap`を指定した場合の画面表示例です。
First Paintの表示とWebフォントダウンロード後の表示で、「Pick Up」のフォントが切り替わっていることが分かると思います。（検証のため、NetWorkの速度を落としています）
![swap.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321338/5bf9aec7-833c-02db-6611-c98a501d23a8.gif)

## Content-Encoding

`Enable text compression`の警告が出ている場合は、Webサーバーのテキスト圧縮の機能を有効にする必要があります。

今回は[compression](https://www.npmjs.com/package/compression#expressconnect)を使用し、gzip圧縮を行いました。
以下Expressでサーバを立ち上げている場合の実装方法例です。

```js:対応前
const express = require('express');

const app = express()

app.use(express.static('public'));
app.listen(3000, () => {
  console.log(`Example app listening at http://localhost:3000`)
})
```

```js:対応後
const express = require('express');

// npm install compression後に、express立ち上げファイルでcompressionを読み込む
var compression = require('compression')

const app = express()

// compressionを実行する
app.use(compression());
app.use(express.static('public'));

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:3000`)
})
```

対象ファイルのResponse Headers記述のContent-Encodingの値がgzipになっていれば成功です。
![gzip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321338/00c0ee58-1bba-797e-5f03-c8e0c326fbee.png)
また、さらに高度な圧縮アルゴリズムであるBrotli形式で圧縮する話も出ましたが、2021年12月時点でexpressの[Brotli対応PR](https://github.com/expressjs/compression/pull/173)がmergeされていないため、gzipで圧縮しています。

## CSS

>**Eliminate render-blocking resources**
>Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles.

レンダリングをブロックしている読み込みファイルが多いとのことなので、ChromeのCoverage機能を利用して不要なファイルや記述をあぶり出していきます。

CoverageタブはDevToolsの検索ウインドウ（「Cmd + Shift + P」で表示）で検索することで表示させることができます。

**参考** : [Find Unused JavaScript And CSS With The Coverage Tab - Chrome Developers](https://developer.chrome.com/docs/devtools/coverage/)

### CSSの削除
Coverageの結果を見ながら、まずは不要なCSSを削除していきます。
各ファイルをクリックすると、Sourceタブにファイル内で使用していないCSS記述を表示してくれるので、不要な記述を削除していきましょう。
ただメディアクエリなどで現在適応されていないCSSもunusedなCSSと表示されることには注意が必要です。
![Screenshot 2021-11-27 1.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321338/892707d8-144e-adc3-00f4-2a028a396884.png)

### Minify

```shell
$ npm install -D cssnano-cli
$ for file in *.css; do npx cssnano < $file > ${file%.*}.min.css; done
```

CSSをminifyしてくれるモジュールである[cssnano](https://cssnano.co/)の[cliバージョン](https://www.npmjs.com/package/cssnano-cli)を使用しています。
上の例ではfor文でループさせてディレクトリ内のすべてのCSSファイルのminifyしています。（パス指定はディレクトリ構造に合わせて変えてください）

## JavaScript

### JavaScriptの削除

JavaScriptもCSSと同様にCoverageタブで未使用のファイルや記述を確認し、それらを削除していく手順となります。

![Screenshot 2021-11-27 1.30.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321338/549be238-6f66-0b9b-fccf-1f8e8664b5e3.png)

Lodashなどのライブラリは、実際に使用する部分以外にも大量の記述やファイルがダウンロードされてしまうため、パフォーマンス悪化に繋がりがちです。
その場合は代替メソッドを手動で定義するなどして、可能な限り使用を控えるようにしましょう。

Lodashであれば、以下GitHubページに各関数の代替記述が載っているので確認してみてください。
[GitHub - you-dont-need/You-Dont-Need-Lodash-Underscore: List of JavaScript methods which you can use natively + ESLint Plugin](https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore)

**参考** : [lodash やめ方 - Qiita](https://qiita.com/mizchi/items/af17f45d5653b76f6751)

### Minify

Webpackを使用してJSファイルをまとめていきます。
まずは必要パッケージをインストールして、

```shell
$ npm install -D webpack webpack-cli
```

このようにWebpackのconfigファイルを最小限で設定します。

```js:webpack.config.js
module.exports = {
  mode: "production",
  entry: `./js/index.js`,
  output: {
    path: `${__dirname}/js`,
    filename: "bundle.js"
  }
};
```

最後にコマンドを実行すればJSファイルが1ファイルにまとまっているはずです。

```shell
$ npx webpack
```

**参考** : [最新版で学ぶwebpack 5入門 - JavaScriptのモジュールバンドラ - ICS MEDIA](https://ics.media/entry/12140/)


## HTML
HTMLのminifyはパフォーマンスに影響しにくい部分でありますが、流れでご紹介。
[html-minifier](https://github.com/kangax/html-minifier)を使うことでCLI上からminifyできます。


```shell
$ npm install -D html-minifier
$ npx html-minifier --collapse-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --remove-tag-whitespace --use-short-doctype --minify-css true --minify-js true index.html > index.min.html
```

公式のsample通りのオプションをつけています。各オプションの内容については以下の記事が詳しいです。

**参考** : [【備忘録】HTMLMinifierの全オプションについて調査した - Qiita](https://qiita.com/ryo_hisano/items/9d41cd447a69943f8eb1)

## まとめ

パフォーマンスチューニングはブラウザの仕組みを正しく把握し、ボトルネックになっている箇所に対して適切に改善を加えていくことが大切です。
ここで学んだ内容を引き続き業務に生かしてきたいですね。

## 全体参考
- [ChromeのLighthouseでページ表示速度を測るときは落とし穴に注意 \| 海外&国内SEO情報ウォッチ](https://webtan.impress.co.jp/e/2018/06/08/29485/page/1)
- [リクルートスピードハッカソンに参加したのでその感想や勉強したことまとめ - Qiita](https://qiita.com/takehanKosuke/items/2f102a069c627c41c907)
- [rel="preload"を極めるために必要な2種類のプリロード機能 \| Raccoon Tech Blog](https://techblog.raccoon.ne.jp/archives/1575956867.html)
- [Google Fonts の表示最適化 - Qiita](https://qiita.com/d2cd-kimura/items/d0565039bd91020af7bd)
- [Google Fontsのパフォーマンスを最適化する6つの方法](https://lpeg.info/webworks/google_fonts.html)