Increments × cyma (Ateam Inc.) Advent Calendar 2020 の1日目は、
Increments株式会社 プロダクト開発グループ Qiita開発チームの花田(@ohakutsu)が担当します!
はじめに
僕は2020年の新卒エンジニアとしてエイチームに入社し、今年の6月にIncrementsに配属されました。
配属後はQiitaの機能開発や改善を行い、現在もQiitaの開発を行っています。
配属後4ヶ月程度の頃、プロダクトマネージャーと話をした際に、
PM「最近はどう?仕事は楽しい?」
僕「つまらなくはないですが、最近新しいことがなくてワクワクすることがないんですよね〜」
PM「あー。じゃあ、花田くんにQiitaの速度改善をお願いしようかな」
僕「あ、やります」
となり、Qiitaの速度改善を託されました。
この記事は、新卒なりにQiitaの速度改善をしたときのことを書いていきます。
速度改善でやったこと
とりあえずLightHouseをやって改善してみたけど...
はじめ、速度改善と言っても何をすればいいのかわかりませんでした。
とりあえずLightHouseで計測をして、「改善できる項目」をとりあえずやってみましたが(プロダクトマネージャーがほとんどやってくれました)、大きな変化を感じることはできませんでした。
速度改善について知る
速度改善がよくわからない状態だったので、超速! Webページ速度改善ガイド ──使いやすさは「速さ」から始まるを読んだりして知識をつけました。
土日にこの本をひたすら読んで、Qiitaのページとにらめっこしていたのを覚えています。
速度改善を始める
速度改善には、リクエスト数を減らす、画像などのリソースの圧縮、ファイルサイズの減量などがあると思いますが、僕が最も重要だと思っているのは、
- クリティカルレンダリングパスの最適化
です。
クリティカルレンダリングパスとは、HTML, CSS, JavaScriptファイルなどのリクエストからレンダリングされるまでの処理のことです。
リクエストからレンダリングまでには、
- HTMLドキュメントのロードと評価
- サブリソースのロードと評価
- レンダリングツリーの構築と描画
があり、それぞれを最適化していくことで、ユーザーがページを表示するまでの時間が短くなっていきます。
ちなみにQiitaのクリティカルレンダリングパスを見ると以下のようになっています。(Chromeの開発者ツール > Performance から見ることができます)
ただ、クリティカルレンダリングパスを最適化すればいいんだなと思っても、どこを改善していけばいいのかがわかりません。
そこで、どこまでの表示を改善するかを先に決めておく必要があります。
わかりにくいので、Qiitaでの具体例を出しつつ説明をします。
ユーザーがページをリクエストしてからページが完全に表示されるまでにはいくつかのステップがあります。
- First Paint
- First Contentful Paint
- First Meaningful Paint
- Time To Interactive
これらのステップのうちのどこまでの速度を改善するかを決めておくことで、改善前後の比較ができるのと、見当違いの箇所を見ることを防げます。
今回、僕は見る範囲をリクエスト開始からTime To Interactiveまでに絞り、速度改善に立ち向かいました。
ちなみに、Time To Interactive
までの時間がQiitaの場合DOMContentLoaded
とほぼ同じなので、DOMContentLoaded
の時間を見ると800ms程度かかっています。(DOMContentLoaded
は最初のHTMLの評価が終わったタイミング)
今回の速度改善のゴールは、DOMContentLoaded
の時間を短くするとします。
ボトルネックな部分の発見
速度改善をするにあたって、何に時間がかかっているのかを知る必要があります。
僕はまず、開発者ツールのPerformanceでリクエスト開始からTime To Interactiveまでの範囲のクリティカルレンダリングパスを見ました。
これを見ると、明らかに初めのHTMLファイル(画像の▼ Network
タブ内のb7a001d26bd98...
のやつ)の時間が500ms程度と長いです。また、何回か計測したところ、300ms〜900ms程度かかっていることがわかりました。
JavaScriptファイル(画像の▼ Network
タブ内のv3-article-bundle-...
)も300ms程度かかっていますが、プロダクトマネージャーが既にファイルサイズの減量をしていたのと、Service Workerでキャッシュをしているので、今回は見送りました。
上の画像から、初めにロードするHTMLファイルが遅いことがわかりましたが、サーバのレスポンスが遅いのかダウンロードに時間がかかっているのか、はたまた評価に時間がかかっているのかは現段階ではわかりません。
そこで、何に500ms程度かかっているのかを見ていきます。
上の画像の▼ Network
タブ内のb7a001d26bd98...
のやつをクリックすると、↓が出てきます。
これを見ると、リクエストからダウンロード完了までに400ms程度、評価に100ms程度かかっていることがわかります。
今度は、約400msもかかっている network transfer のほうを見ていきます。
Chromeの開発者ツール > Networkから、b7a001d26bd98...
のやつを選択して、Timingを見ると、
Waiting(TTFB)
とContent Download
に多くの時間がかかっていることがわかります。(400msのうちの150msはどこにいったのかはわかってないです...)
ちなみに、Waiting(TTFB)
はリクエストを送ってから初めのレスポンスを受け取るまでの時間です。要因として考えられるのは、サーバでの処理に時間がかかっているなどです。
実際に、Qiitaのサーバサイドで時間がかかっているのかを見たところ、リクエストを受けてからレスポンスをするまでに100ms〜200ms程度かかっていることがわかりました。
ボトルネックになっている部分を見つけ出したので、そこの改善を試みます。
ボトルネックな部分を改善したいけど...
ボトルネックな部分を見つけたのはいいのですが、結論から言うと改善をすることができませんでした。
記事のページに関する部分に
- N+1のクエリなどがあるのだろうか?
- 時間のかかる処理が隠れているんだろうか?
- SSRが問題になっているのだろうか?
と思い調査をしましたが、結局原因がわかりませんでした。
Resource Hintsで事前読み込みをする
ボトルネックな部分を見つけることはできましたが、改善をすることができませんでした。
リクエストからレスポンスが遅く、解決できないのであれば事前読み込みをするしかありません。
そこで、Resource Hints
を使い、事前読み込みをします。
Resource Hints
はブラウザの機能にあるもので、未来に使うリソースを事前に読み込んでおくことができます。
Resource Hints
については、記事を書いているのでぜひご覧ください↓
今回、事前読み込みをするのは、トレンドページに載っている記事のアクセスが多いのと、試験的にやるということで、トレンドページに載っている記事にしました。
また、事前読み込みをするタイミングは、いくつか試した結果、トレンドページの記事へのリンクをホバーしたときに事前読み込みが走るようにしています。
以下の画像で、記事へのリンクをホバーしたときに事前読み込みが走っていることがわかります。
また、Qiita開発チームでは以下のようなカスタムフックを実装し、Resource Hints
に必要な処理を意識しなくて良いようにしています。
import { useCallback, useState } from 'react'
interface Props {
rel: 'dns-prefetch' | 'preconnect' | 'prefetch' // prerender は1つのページしか事前レンダリングできないため使わない
url: string
resourceType: 'document' | 'image' | 'script' | 'style'
}
export const useResourceHints = ({ rel, url, resourceType }: Props) => {
const [isFetched, setIsFetched] = useState(false)
const fetchResource = useCallback(() => {
if (document && !isFetched) {
const linkElement = document.createElement('link')
linkElement.rel = rel
linkElement.href = url
linkElement.as = resourceType
document.head.appendChild(linkElement)
setIsFetched(true)
}
}, [isFetched, rel, resourceType, url])
return { fetchResource }
}
import React from 'react'
import { useResourceHints } from './path/to/useResourceHints'
interface Props {
title: string
url: string
}
export const SampleArticleLink = ({ title, url }: Props) => {
const { fetchResource } = useResourceHints({
rel: 'prefetch',
url: url,
resourceType: 'document',
})
return (
<a href={url} onMouseEnter={fetchResource}>
{title}
</a>
)
}
結果
Resource Hints
を使った事前読み込みで、表示速度が速くなったかを見ます。
Before | After |
---|---|
ゴールに設定していたDOMContentLoaded
を見ると、明らかに速くなっていることがわかります。
Resource Hints 最高!
おわりに
新卒のエンジニアがQiitaの速度改善をした話を書いてきました。
結果としては、すべてのページの速度が速くはなりませんでしたが、ユーザーがよく見るトレンドページからの遷移を速くすることができました。
ここ間違ってるよ、こうするといいよなどがありましたらコメントいただけると嬉しいです。
以上が新卒1年目のエンジニアがQiitaの速度改善をした話でした。
Increments × cyma (Ateam Inc.) Advent Calendar 2020 の2日目は、株式会社エイチーム EC事業本部の@shimabeeがお送りします!!