💾
ブラウザでローカル保存したい / OPFS Web Worker編
はじめに
最近Webブラウザからローカル保存するOPFSというものにハマってます。
前回 はかんたんに読み書きする方法を調べてみました。
その後も調べてみると、ファイルを排他制御した上で同期的に書き込む createSyncAccessHandle() というAPIを見つけました。
パフォーマンス上有利みたいですね。
毎回 Promise を解決するオーバーヘッドがなく、直接ファイルシステムに書き込めるからでしょうか??
マウスを動かすたびにx,y座標をevents.jsonlに書き込み、space keyを押すとVector計算した結果を出力するものを作って試してみました。
Web Worker上で動くぞ OPFS
createSyncAccessHandleはWeb Worker上でしか動かないみたいです。
同期的に書き込むとUIをロックしてしまうからでしょうかね??
さっそく環境を作ってみましょう。
僕は pnpm create vite . で作っています。
言語はTypeScriptの方が好みですが、どっちでもいいです。
もしtsを使う場合はtsconfig.jsonのlibに WebWorker を追加しましょう。
こんな感じです。
"lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
まずはmain thread
import Worker from './worker?worker'
const worker = new Worker()
window.addEventListener('mousemove', event => {
const data = {
x: event.clientX,
y: event.clientY,
}
worker.postMessage({
data: JSON.stringify(data),
})
})
window.addEventListener('keydown', async event => {
if (event.code !== 'Space') {
return
}
const root = await navigator.storage.getDirectory()
const handle = await root.getFileHandle('events.jsonl')
const file = await handle.getFile()
const content = await file.text()
const lines = content.trim().split('\n').filter(Boolean)
const result = lines.reduce(
(acc, line) => {
const { x, y } = JSON.parse(line)
return { x: acc.x + x, y: acc.y + y, events: acc.events + 1 }
},
{ x: 0, y: 0, events: 0 }
)
console.log(result)
})
viteでのworkerのimportは ?worker が必要ですので忘れず入れてください。
次にWeb Worker
let accessHandle: FileSystemSyncAccessHandle | null = null
let writePosition = 0
self.addEventListener(
'message',
async (event: MessageEvent<{ data: string }>) => {
const { data } = event.data
if (!accessHandle) {
const root = await navigator.storage.getDirectory()
const draftHandle = await root.getFileHandle('events.jsonl', {
create: true,
})
accessHandle = await draftHandle.createSyncAccessHandle()
// 初回に最後のpositionを取得し、以降encodeしたbyteの長さを足してwriteメソッドに引き渡す
writePosition = accessHandle.getSize()
}
const line = `${data}\n`
const encoded = new TextEncoder().encode(line)
// 同期的に書き込んでる
accessHandle.write(encoded, { at: writePosition })
writePosition += encoded.byteLength
accessHandle.flush()
}
)
まとめ
createSyncAccessHandleを使ってOPFSに同期的に書き込めました。
前回の createWritable と比べると:
| createWritable | createSyncAccessHandle | |
|---|---|---|
| 動作場所 | どこでも | Web Workerのみ |
| 書き込み | 非同期(await必要) | 同期(await不要) |
| 排他制御 | なし | あり |
| パフォーマンス | 普通 | 高速 |
今回はmousemoveのたびに書き込むという激しい使い方をしましたが、問題なく動きました。
append-onlyでログを残す使い方には良いのですが、集計は結構しんどいのでProjection用にDuckDBやSQLiteのwasmを使ってみたいですね。
Discussion