ISRでグラフを表示する

# はじめに

Webフロントエンド技術の勉強をしていて、ISR(Incremental Static Regeneration)というのがあるのを知りまして、これをIoTに応用したらどうなるかな、と考えてみたところ、**「センサーデータを表示するグラフやダッシュボードをISRで作ったら高速でかつ(ある程度は)最新情報を反映したページが作れるのでは」**というアイデアを思いつきましたので、形にしてみました。

結果はこんな感じでかなり応答時間が改善されます。
![third.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/c6e620c8-de09-6b67-a5c1-ff68f6f73f13.png)

# ISRとは

ISR(Incremental Static Regeneration)というのは、

- 段階的に(Incremental)
- 静的なサイトを(Static)
- 再生成する(Regeneration)

というもののようです。

その前にSSG(Static Site Generation)というのがあります。こちらはビルド時に情報を取得しつつ、必要なページを生成する、という技術で、アクセス時にサーバー処理が必要ないため軽く、かつ静的サイトなのでCDNやブラウザのキャッシュも使いやすい、というものです。

ただこれだとビルド時から情報が変わらないので、ブログサイトのようなほぼ内容が固定されるのであればいいのですが、情報が変わりうるサイトだと使用できません。

そのような、静的サイトとして提供したいんだけど、変動する情報もある程度は扱いたい、という場合に使えるのがISRです。こちらはビルド時にいったん静的ページを生成し、アクセスがあったらその静的ページを返すのですが、アクセス時にページ生成時間が古くなっていたらバックグラウンドで静的ページを再生成し、次回アクセス時には再生成したページを返す、という仕組みです。SSGの静的ページであることの恩恵を受けつつ、ある程度の遅れは出るが変動する情報にも対応できるページを作ることができます。

詳しくはこちらの記事にて詳しく説明されていますので、ご覧いただければと思います。

[Next.jsにおけるSSG（静的サイト生成）とISRについて（自分の）限界まで丁寧に説明する](https://qiita.com/thesugar/items/47ec3d243d00ddd0b4ed#%E3%82%A4%E3%83%B3%E3%82%AF%E3%83%AA%E3%83%A1%E3%83%B3%E3%82%BF%E3%83%AB%E9%9D%99%E7%9A%84%E5%86%8D%E7%94%9F%E6%88%90-incremental-static-regeneration-isr)

# 作るもの

このISRをちょっと試してみたいと思い、題材を考えたところ、センサーデータのグラフをISRで表示することを思いつきました。

日頃からSORACOM関係のブログを書いているので、クラウドにデータをアップロードできるセンサー類はいっぱい持ってます。特に[GPSマルチユニット](https://soracom.jp/products/kit/gps_multiunit/)はプログラミングなしで使えるので、こういうちょっと試したみたいという時には最適なアイテムです。

![gpsmultiunit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/d07d52e9-1f91-98b1-b201-0b6166e38a98.png)

このデータをAmazon Timestreamに保存します。Timestreamは時系列データを保存するのに最適化されたAWSのデータベースで、ほんのちょっとの準備でいい感じに使うことができます。最近一般公開されたばかりのサービスで、現状では色々足りないところもありますが、今後期待しているサービスです。

このTimestreamからデータを取得して静的ページに埋め込み、グラフで描画するアプリを作成します。データはビルド時に確定しますが、ISRの機能によって更新されていくというわけですね。WebアプリはVercelにホストします。簡単にデプロイできて、まともにISRを使えるのは今のところVercelしかないっぽいので。

構成はこんな感じです。

![isr-graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/29382666-30b0-300a-5210-e9dd35b41e08.png)

# 開発

データをTimestreamに保存するところまでは、以下のブログの通りにしました。とても簡単で素晴らしいです。データベース名はsoracom、テーブル名はgpsmultiunitとしました。
[最小限のデバイス開発でデータをAmazon Timestreamに送るSORACOMとAWSの構成](https://blog.soracom.com/ja-jp/2020/10/06/iot-architecture-with-mazon-timestream-and-soracom/)

ISRを使いたいため、WebアプリにはNext.jsを使います。Javascriptのグラフツールは色々ありますが、今回はChart.jsを使いました。以下のコマンドを実行して、必要な環境を整えます。

```bash
npx create-next-app
npm install react-chartjs-2 chart.js aws-sdk
```

index.jsを以下の内容に書き換えます。

```javascript
import React from 'react';
import { Line } from 'react-chartjs-2';
import AWS from 'aws-sdk'

const colors = ['red', 'blue', 'green', 'orange', 'purple', 'yellow']

function Graph( { graphData } ) {
  const datasets = Object.keys(graphData.data).map((key, index) => {
    return {
      label: key,
      fill: false,
      showLine: true,
      lineTension: 0,
      borderColor: colors[index % colors.length],
      data: graphData.data[key]
    }
  })

  const data = {
    labels: ['Line'],
    datasets: datasets
  };  

  return (
    <div>
      <div>グラフ {graphData.time.build}</div>
      <Line
        data={data}
        width={400}
        height={100}
        options={{
          animation: false,
          scales: { xAxes: [{
            type: "time",
            distribution: "linear",
            ticks: {
              min: graphData.time.min,
              max: graphData.time.max
            }
          }]},
        }}
      />
    </div>
  );
};

export async function getStaticProps() {
  const client = new AWS.TimestreamQuery({
    region: "us-east-1",
    credentials: {
      accessKeyId: process.env.ACCESS_KEY_ID,
      secretAccessKey: process.env.SECRET_ACCESS_KEY
    }
  });

  const params = {
    QueryString: 'select measure_name, time, measure_value::double from "soracom"."gpsmultiunit" where time > ago(60m) order by time asc'
  }

  const graphData = { time: { build: new Date().toString()}, data: {}};
  await client.query(params).promise()
  .then(
    (response) => {
      response.Rows.forEach(row => {
        if (!graphData.data.hasOwnProperty(row.Data[0].ScalarValue)){
          graphData.data[row.Data[0].ScalarValue] = []
        }
        graphData.data[row.Data[0].ScalarValue].push({x: row.Data[1].ScalarValue, y: row.Data[2].ScalarValue})
      })
      graphData.time.min = response.Rows[0].Data[1].ScalarValue
      graphData.time.max = response.Rows[response.Rows.length - 1].Data[1].ScalarValue
    },
    (err) => {
      console.error("Error while querying: ", err);
    }
  )

  return {
    props: {
      graphData
    },
    revalidate: 60
  }
}

export default Graph
```

getStaticPropsメソッドにて直近1時間のTimestreamに保存されたデータをクエリし、必要なグラフデータに加工して、そのデータをGraphメソッドにてグラフ描画しています。getStaticPropsメソッドの戻り値にてrevalidateを指定していますが、これがISR用の設定です。GPSマルチユニットは最短1分でデータをアップロードするため、60秒以上古いページであれば再生成する、という動きにしています。

一点うまくいかなかったところは、Timestreamのクライアントでcredentialsを指定しているのですが、これはVercelではAWS_ACCESS_KEY_IDやAWS_SECRET_ACCESS_KEYを環境変数として指定するのが禁止されているためです。別の環境変数を用意して対応しましたが、ちょっといまいちな感じはしますね。

このコードをGithubなどの適当なリポジトリに上げ、VercelにDeployすれば準備完了です。環境変数にACCESS_KEY_ID、SECRET_ACCESS_KEYを忘れず入れるようにしましょう。

# 動作確認

まずは初回アクセスです。確認のためキャッシュをクリアしてからアクセスしました。
![first.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/51d73490-8d9c-bf6a-918a-f49a1261b428.png)

まずちゃんとグラフは表示されていますね。ただし古いデータが表示されています。初回は何もキャッシュされていないため、ダウンロードなどにやや時間がかかっています。Finishまでの時間は593msです。

次は２回目のアクセスです。
![second.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/46604742-01d3-8fb0-e6c4-d61b2fbec0a8.png)

新しいグラフに更新されていることが確認できました。(グラフの色が入れ替わってしまった)またライブラリなどの他のファイルはdisk cacheから取得しており、ネットワークアクセスが発生していないことが確認できました。Finishまでの時間は394msです。

60秒あけずに次のアクセスをしました。
![third.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/c6e620c8-de09-6b67-a5c1-ff68f6f73f13.png)

メインのページisr-graph.vercel.appが304(Not Modified)になっていることが確認できましたね。これはページ自体が小さいので時間的にあまり変わりませんが。。アクセスまでの時間が短かったためか、他のファイルがmemory cacheになっており、そのためFinishまでの時間が294msになっています。これはどちらかといえばページが再生成されないことの確認だったのでこれでいいでしょう。

比較のため、SSR(Server Side Rendering)でもやってみましょう。

先ほどのコードのgetStaticPropsをgetServerSidePropsに変更し、戻り値のrevalidate属性を削除すれば良いです。

まずキャッシュを飛ばした状態です
![ssr1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/109b5305-cb39-3e55-5ab8-fa6a6c14dc89.png)

メインのページisr-graph.vercel.appの取得に時間がかかっていることが分かりますね。これはサーバー側でTimestreamからクエリしており、その時間がまあまあかかるためです。Finishまでの時間は1.05sです。また、最初のファイルが届かないのでページ遷移に時間がかかっているように見えます。

続けてアクセスします。
![ssr2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/1aeb9f9f-beb6-d0f3-a436-29c59c7ff1ca.png)

まず同じデータで時間が違います。これはSSRでは都度データを作っているためですね。また、やはりメインのページの取得に同じ程度の時間がかかっていることがわかります。アクセスの都度Timestreamにクエリしていることがわかります。Finishまでの時間は888ms。他のファイルがキャッシュされたためやや短くなりましたが、メインページの遅さに引っ張られています。

こういう場合、ページ遷移の遅さを避けるために、先にグラフの枠を表示しておいてから中身はAjaxで取得する、という手法が取られるのが一般的かと思いますが、この場合グラフが描画されるまでの時間はSSRよりもさらに遅くなります。ISRを使うと最初にデータ詰め込んで渡せるので、かなり改善できることがわかりました。

# 結果の考察

応答時間という意味でISRはとても良いです。
また副産物としてTimestreamへのクエリコストを減らせるという面もあります。([Timestreamのクエリ料金](https://aws.amazon.com/jp/timestream/pricing/)は10MBを最小単位とするため、直近の数kB程度のデータを取得するのにも$0.0001 ≒ ¥0.01かかります。安いといえば安いのですが、無視できるほど安いかというとそうでもないです。アクセスが多い場合は特に)

CDNやサーバーでのキャッシュでも似たようなことができそうなんですが、その辺りとのすみわけはどう考えるべきなんでしょうか？このあたりはまだまだ勉強不足です。

懸念点としては、初回アクセス時にかなり古いデータが表示される可能性があることです。1分更新のデータで、グラフが3分前とかであればまあいいかと思うのですが、1時間前、1日前のデータだと流石に古すぎる感じがします。これはキャッシュを返して、裏で静的ページを生成する仕組み上避けられないですね。常に一定のアクセスがあるページだといいのですが、そうでない場合は古すぎるデータが表示された場合強制的に再取得するような仕組みは必要になるかと思いました。

また、これは単に勉強不足なだけかもしれませんが、SSGやISRで認可制御ってどうやるのかが分かりません。ユーザーの属性に応じて表示やデータを削除することはできると思うのですが、必要な情報がページ内に全部来てしまうので、厳密な制御は無理なのではないかと思っています。ページごと見れる / 見れないの制御はできるのかな？ここは調べておいた方がいいと思いました。

なので、このISRでグラフ表示するのに向いたユースケースは、ある程度アクセスがある一般公開されたサイトでなんらかのリアルタイムグラフを表示する場合、という感じになるかと思います。なんだろう。IoTのセンサーやCPU使用率などのダッシュボード、他には株価とかかな？

# おわりに

フロントエンドは素人なので色々調べながらやりましたが、いろいろな技術が出てきていて面白いですね。ちょっとは使えるようになりたいものです。