1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1000ページのPDFをフリーズなく表示するTurbo View Engineを設計した話【開発日誌 #4】

1
Last updated at Posted at 2026-04-20

開発日誌 #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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?