開発日誌 #4 です。前回はAES-256-GCMとZero Leak Architectureの話を書きました。
今回は「重いPDFをサクサク表示する」という、一見地味だけど実装が一番大変だった部分の話です。
※検証環境は8年前のMacBook Airです。
問題:普通にやるとすぐ固まる
最初の実装はシンプルでした。PDFを開いたら全ページをレンダリングしてDOMに突っ込む。
100ページくらいまでは問題なし。
300ページを超えたあたりから重くなり始め、1000ページになるとアプリが数秒固まります。
原因は明白で、見えていないページまで全部レンダリングしているからです。
解決策:仮想化スクロール
Webフロントエンドでは一般的な手法ですが、PDFビューアに適用する場合は少し工夫が必要です。
基本的な考え方:
表示領域(viewport)に入っているページだけをレンダリングする
↓
スクロールしたら、見えなくなったページのDOMを破棄
↓
新しく見えてきたページをレンダリング
実装上のポイントは「ページの高さをレンダリング前に知る必要がある」ことです。
PDFのページサイズはページごとに異なる場合があるので、lopdfでページのMediaBoxを先読みしてサイズだけ取得します。
pub fn get_page_sizes(doc: &Document) -> Vec<(f64, f64)> {
doc.page_iter().map(|page_id| {
let page = doc.get_object(page_id).unwrap();
// MediaBoxからwidth/heightを取得
let media_box = page.as_dict()
.and_then(|d| d.get(b"MediaBox"))
.and_then(|o| o.as_array())
.map(|arr| (
arr[2].as_float().unwrap_or(595.0),
arr[3].as_float().unwrap_or(842.0),
))
.unwrap_or((595.0, 842.0));
media_box
}).collect()
}
これでレンダリング前にスクロール領域の総高さを計算できます。
Ghost Batch:プロセス生成コストを90%削減
仮想化スクロールだけだと、スクロールのたびにページレンダリングが走って引っかかりが出ます。
ここで導入したのが Ghost Batch です。
通常の実装:
スクロール → ページA描画リクエスト → プロセス起動 → 描画 → 返却
スクロール → ページB描画リクエスト → プロセス起動 → 描画 → 返却
Ghost Batch:
スクロール → キューに積む
キューが一定量たまったら → バッチで一括処理
複数のページ描画リクエストを束ねて一度に処理することで、プロセス起動のオーバーヘッドを大幅に削減できます。
Intelligent Prefetch:次のページを先読み
スクロール方向を検知して、次に表示される可能性が高いページをバックグラウンドでレンダリングしておきます。
const handleScroll = (e: React.UIEvent) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
const direction = scrollTop > prevScrollTop.current ? 'down' : 'up';
const visiblePages = getVisiblePages(scrollTop, clientHeight);
// スクロール方向に応じて先読みページを決定
const prefetchPages = direction === 'down'
? [visiblePages.last + 1, visiblePages.last + 2]
: [visiblePages.first - 1, visiblePages.first - 2];
prefetchPages
.filter(p => p >= 0 && p < totalPages)
.forEach(p => prefetchPage(p));
prevScrollTop.current = scrollTop;
};
結果
| 指標 | 改善前 | 改善後 |
|---|---|---|
| 1000ページPDF読み込み | 約8秒フリーズ | ほぼ即時 |
| メモリ使用量 | 全ページ分 | 表示ページ×3程度 |
| スクロール時のカクつき | あり | ほぼなし |
現在の状況(dev版)
1000ページを超えるPDFでもスムーズに動作しています。
次回
次回は Magic Pipeline(自動化ワークフロー)の設計について書きます。
OCR → 圧縮 → 保存 のような複数処理を1クリックで繋げる仕組みです。
Hiyoko PDF Vault(日本語) → https://hiyokoko.gumroad.com/l/HiyokoPDFVault_jp
X → @hiyoyok