はじめに
おーせと申します。6月からエンジニアになったばかりですが、コツコツアウトプットしようと思い記事を投稿します。
未経験転職の際にポートフォリオをReact・Railsで作ったものの、キャッシュ設計について何も考えておらず、脳死でaxiosとuseState,useEffectを駆使してアプリケーションを作りました。
結果SPAなのにもっさりした挙動になってしまい、悲しみに暮れていたところ、友達がこんな記事を紹介してくれました。
そこで初めてSWRの存在とサーバーキャッシュの管理というものを知りました。
駆け出しエンジニアではありますが、SWRについて調べたことについて整理したいと思います。
SWRとは
以下公式のドキュメントになります。
ドキュメントの冒頭に以下のように説明があります。
“SWR” という名前は、 HTTP RFC 5861 で提唱された HTTP キャッシュ無効化戦略である stale-while-revalidate に由来しています。 SWR は、まずキャッシュからデータを返し(stale)、次にフェッチリクエストを送り(revalidate)、最後に最新のデータを持ってくるという戦略です。
後半の文章を抜粋してSWRの役割を整理したいと思います。
キャッシュからデータを返す(stale)
キャッシュとは、利用率の高いデータを高速な記憶装置に保存しておくことで、データ処理を高速化する技術で、リソース取得の高速化、サーバへの負荷軽減といった利点があります。
SWRでは、まずキャッシュにアクセスし、データをフェッチするといった処理を行います。
フェッチリクエストを送る(revalidate)
revalidateとは、「再検証」という日本語訳をします。
アクセスするキャッシュの情報が古い場合、それを更新する必要があります。SWRでは古いキャッシュを新しくするためにサーバー側にフェッチリクエストを送ります。
最後に最新のデータを持ってくる。
サーバーから取得した最新のデータをもとに、キャッシュの更新を行います。
下が対応関係を図で表したものです。
ちなみに、上記staleとrevalidateは非同期で処理されるため、キャッシュを最新の状態に保ちつつ、そのキャッシュからデータを返すということが実現されます。
useSWRの使い方
基本的にデータフェッチにはuseSWRを使います。呼び出す形としては以下のコードを書きます。
const { data, error } = useSWR(key, fetcher)
keyは非同期関数であるfetcherの引数として渡され、取得したデータをdataとして返します。
errorはオプションの一つでキャッシュアクセス時に発生したエラーが発生した場合に値を持つものです。エラーがなければundefinedを返します。
以下の実装が公式に載っているので参考までに載せておきます。
import fetch from 'unfetch'
const fetcher = url => fetch(url).then(r => r.json())
function App () {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
比較検証
ここで、SWRを使うパターンと使用しないパターンのデータ取得にかかる処理時間を比較検証してみます。そうすることで使用するメリットが理解しやすいかもと思いました。
例えば以下のjsonを返すエンドポイント/tasks
があるとします。
[
{"id" : "1", "title" : "aaa"},
{"id" : "2", "title" : "bbb"}
]
それぞれ以下のコードを用意してみました。
ケース1: axiosのみ使用した場合
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import { Box, Text, Button } from '@chakra-ui/react'
const Axios = () => {
const [loading, setLoading] = useState(true)
const [tasks, setTasks] = useState()
useEffect(() => {
const before = performance.now();
axios.get('http://localhost:8000/tasks').then(res => {
const after = performance.now();
setTasks(res.data)
setLoading(false)
console.log("処理時間は、" + (after - before) + "ミリ秒です");
}).catch((err) => {
console.log(err)
})
}, [])
if (loading) return <>...loading</>
return (
<>
<Box flexDirection='column'>
<Box>
{ tasks.map((task) => (
<Text key={task.id}>{task.title}</Text>
))}
</Box>
<Button onClick={() => {window.location.reload()}}>リロード</Button>
</Box>
</>
)
}
export default Axios;
ケース2:SWRを使用した場合
import React from 'react'
import axios from 'axios'
import useSWR from 'swr'
import { Box, Text, Button } from '@chakra-ui/react'
const fetcher = (url) => axios.get(url).then(res => res.data)
const Swr = () => {
const before = performance.now();
const { data: tasks } = useSWR('http://localhost:8000/tasks', fetcher)
if (!tasks) return <>...loading</>
const after = performance.now();
console.log("処理時間は、" + (after - before) + "ミリ秒です");
return (
<>
<Box flexDirection='column'>
<Box>
{ tasks.map((task) => (
<Text key={task.id}>{task.title}</Text>
))}
</Box>
<Button onClick={() => {window.location.reload()}}>リロード</Button>
</Box>
</>
)
}
export default Swr;
どっちの見た目も下のようになり、リロードボタンを押すと画面が更新され再度処理が実行されます。
また、今回は以下のコードを取得処理の前後に挟むことで処理時間を計測してみました。
(検証ツールを使用する方法が一般的かもですがやり方がよくわかりませんでした...😅)
const before = performance.now();
<取得処理>
const after = performance.now();
console.log("処理時間は、" + (after - before) + "ミリ秒です");
それぞれ結果は以下の通りでした。2回出力されているのはレンダリングされる回数の問題です。
(共にリロードを押した時のコンソール出力結果を表示しています。)
処理方法 | コンソール出力結果 |
---|---|
ケース①axiosのみ | 処理時間は、96.30000001192093ミリ秒です 処理時間は、97.30000001192093ミリ秒です |
ケース②SWR活用 | 処理時間は、0.5ミリ秒です 処理時間は、0.10000002384185791ミリ秒です |
リロードする度、多少のムラはありますが差があります。
axiosのみの場合はサーバーまで取得しにいっているのに対して、SWRはキャッシュからデータを取得しているため、この違いが発生しています。
本番環境だとよりこう言った差がより大きくなるのでは、と思うので、キャッシュをうまく使うことはパフォーマンス向上のために重要なことだと思いました。
最後に
最近SWRをかじった程度でしたが、実際に触ったり公式ドキュメントを見ながらまとめてみました!ここまで読んでいただいてありがとうございます🙇♂️
今回はuseSWRだけでしたが、今後useSWRMutaionなども理解を深めて記事を投稿できたらと思います😊
それでは!
参考文献