はじめに
記録時点: 2026-01〜2026-02
この時期の課題は、機能不足ではなく体感速度でした。 処理は完了しているのに「固まったように見える」時間が増え、 連続編集のテンポが落ちていました。
この記事で扱う範囲
- 改ページ再計算が広範囲に波及していた問題
- ローディング表示の出し方が体感を悪化させていた問題
- 初回表示に重い依存が集中していた問題
最適化テクニックの紹介ではなく、待ち時間の発生源を分離した記録です。
症状の出方
1. 軽い編集でも待ちが長い
1つの操作で広い範囲に再計算が波及し、 入力テンポを保てない状態でした。
2. ローディングの点滅が増えて遅く感じる
短時間処理でもローディングを毎回表示すると、 実時間以上に「待たされ感」が強くなります。
3. 初回操作に入るまでが重い
依存ライブラリの読み込みが集中し、 最初の1回目操作が特に鈍くなっていました。
先に決めた方針
この回では、速度改善を1つのスコアで追わず、次の3層で分離しました。
| 層 | 見る指標 | 代表症状 |
|---|---|---|
| 再計算 | 操作後の応答遅延 | 軽い編集でも待つ |
| 表示制御 | ローディング発生頻度 | 点滅して遅く感じる |
| 初回ロード | 初回操作までの遅延 | 起動直後が重い |
実装で変えたポイント
1. 改ページ計測中の操作を抑止した
まず破綻を防ぐため、計測中の操作を受け付けないようにしました。 状態に応じてメッセージを分け、待ち理由を明確化しています。
function getBusyMessage(state: BusyState): string | null {
if (state === "loading") return "プリントを読み込んでいます...";
if (state === "reflow") return "プリントを再描画しています...";
return null;
}2. 再計算範囲を差分基準へ変更した
全体再計算を減らし、変更位置から必要範囲だけ再計算するように切り替えました。
export function resolveReflowRange(changedIndex: number, pageMap: number[]): [number, number] {
const start = Math.max(0, changedIndex - 1);
const end = Math.min(pageMap.length - 1, changedIndex + 2);
return [start, end];
}3. ローディング表示に遅延閾値を入れた
200ms未満の処理ではローディングを出さず、 待ちが発生するケースだけ可視化する方式に変更しました。
export async function withDelayedSpinner<T>(task: () => Promise<T>, show: () => void, hide: () => void): Promise<T> {
const timer = setTimeout(show, 200);
try {
return await task();
} finally {
clearTimeout(timer);
hide();
}
}4. 初回読み込みの偏りを分散した
重い依存を読み込み単位ごとに分割し、初回表示の集中を緩和しました。
見送った案
1. とにかくローディングを消す
見た目は軽くなりますが、実際の待ち理由が分からなくなり運用で混乱するため見送りました。
2. 全体再計算を維持したままキャッシュで押し切る
ケースが増えるほど破綻しやすく、保守コストが高いため採用しませんでした。
3. 初回ロードの重さを後続改善へ先送りする
最初の体感が悪いと離脱率が高いので、この回で同時に手を入れました。
回帰で固定した観点
- 連続追加/並び替えで操作待ちが連鎖しない
- ローディングが不要な短時間処理で点滅しない
- 計測中に次操作を受けず、状態が壊れない
- 初回表示後の最初の操作で極端な遅延が出ない
この回の判断が次に効いたこと
「待ちを分解して直す」方針は、次の2記事にも直結しています。
まとめ
今回の改善で効いたのは、1つの最適化を深掘ることではなく、 再計算・表示・初回ロードを別問題として扱ったことでした。 その結果、編集画面の「重い」を体感レベルで減らせています。