- 実行時間の測定を通じて不要なデータバインディング処理を特定し、改善した経験 -
1. 問題の状況
プロジェクトでは、ReactとTypeScriptを基盤としてExcel業務画面を実装しており、ExcelファイルをWeb上で閲覧・編集できるようにSpreadJS(v17.0.1)を使用しています。ユーザーがアップロードしたExcelファイルをテンプレートとして保存し、詳細画面ではそのテンプレートに実際のデータを連携して画面に表示する構成です。
問題は、Excel詳細画面に入る際の初期ロード時間が極端に長いことでした。画面に入ってもすぐにExcelが表示されず、まず空白画面が表示された後にローディング画面が表示され、さらに一定時間が経過してからExcel画面が表示されました。開発環境で測定した結果、全体のロード時間は約25~30秒でした。使用しているExcelファイルは合計28個のシートで構成されており、シート間の参照や数式なども含まれているため、構造が複雑でした。
このように複雑なロード過程の中で、どの区間が実際に時間を消費しているのかを段階的に測定し、ボトルネックを特定して改善した過程を共有します。
2. Excelファイルおよび行・列範囲の確認
最初に、Excelファイルのサイズとシートの行・列範囲を確認しました。以前SpreadJSを使用した際、実際のデータより不要に大きく設定された行・列範囲がロード性能に影響していた問題を改善した経験があったため、今回も同じ原因である可能性をまず確認しました。
(※以前の行・列範囲に関する性能改善事例は https://www.nextree.io/spreadjs-egsel-roding-seongneung-coejeoghwa/ で確認できます。)
以前の事例では、実際のデータは約100行に過ぎませんでしたが、Excel内部のシートの行範囲が最大行である1,048,576行まで設定されていました。実際のデータとは関係なく広い範囲を処理していたためロード時間が増加しており、不要に設定されていた範囲を縮小することで改善しました。
今回もExcelの行・列範囲を確認した結果、一部のシートの範囲が実際のデータに比べて大きく設定されていました。プロジェクトでは、Excelの入力範囲を最大1,000行、1,000列まで使用するよう定めていたため、その範囲に制限して保存するテストを行いました。その結果、SJSデータの容量は次のように減少しました。
- 修正前:約5.35 MB (5,357,864 bytes)
- 修正後:約4.67 MB (4,675,857 bytes)
しかし、ファイルサイズが減少したにもかかわらず、ロード時間は大きく短縮されませんでした。したがって、今回の問題の主な原因は単純なExcelファイルのサイズや行・列範囲ではないと判断しました。以前はExcel自体の不要な範囲を縮小することで性能を改善しましたが、今回はExcelを読み込んだ後に画面上で実行される処理を確認する必要がありました。
3. ロード過程ごとの実行時間測定
ファイル自体が原因でないのであれば、実際にどの処理で時間が発生しているのかを確認する必要がありました。そこで、Excelを画面に表示する過程を複数の段階に分け、実行時間を測定しました。代表的な測定結果は次のとおりです。
|
処理 |
所要時間 |
|---|---|
|
workbook.open |
約2.3秒 |
|
BindingPath情報の構成 |
約25ms |
|
BindingPathデータの加工 |
約1ms |
|
CellBindingSourceの生成 |
約2ms |
|
setDataSource |
約18秒 |
結果は予想に反して、Excelファイルを実際に読み込む`workbook.open()`には約2.3秒かかりましたが、その後にBindingPathを確認したりデータを加工したりする過程は、ほとんどが数十ミリ秒以下でした。一方、`setDataSource()`を実行する区間では約18秒を要していました。これにより、今回のロード遅延の主な原因はExcelファイルを読み込む処理ではなく、Excelを読み込んだ後にデータをシートへ連携する処理にあることを確認しました。
4. 全シートのデータバインディング処理の確認
原因をさらに具体的に確認するため、既存のコードを調査しました。既存の実装では、すべてのシートを対象に`setDataSource()`を実行していました。
const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(data);
for (let i = 0; i < workbook.getSheetCount(); i++) {
const sheet = workbook.getSheet(i);
sheet.setDataSource(dataSource);
}
今回のExcelは合計28個のシートで構成されていましたが、実際にデータバインディングが必要なシートは一部だけでした。
|
[既存の構成]28個の全シートを走査 -> すべてのシートでsetDataSource()を実行 |
|---|
ここで、シートごとの実行時間を測定してみました。
- Sheet 1:約1,021ms
- Sheet 2:約836ms
- Sheet 3:約805ms
- Sheet 4:約812ms
- Sheet 5:約783ms
- Sheet 6:約771ms
シート1つの処理に約0.7~0.9秒かかっていました。さらに重要なのは、バインド対象のパス(BindingPath)がないシートでも、`setDataSource()`の呼び出しにかなりの時間がかかっていたことです。したがって今回の問題の主な原因は、バインディングが不要なシートに対しても同じ処理を繰り返し実行していたことだと判断しました。
ただし、なぜバインディングパスがないシートでもsetDataSource()の呼び出し自体に時間がかかるのかについては、SpreadJS内部の動作を今後別途検証する予定です。
5. データバインディング対象シートの選別
従来はすべてのシートで`setDataSource()`を実行していましたが、実際にデータバインディングの対象となるシートだけを選別して処理するよう、構成を変更しました。
const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(data);
for (let i = 0; i < workbook.getSheetCount(); i++) {
const sheet = workbook.getSheet(i);
if (!sheet.visible()) {
continue;
}
if (!isBindingTargetSheet(sheet)) {
continue;
}
sheet.setDataSource(dataSource);
}
- 他のシートの数式・参照用としてのみ使用される非表示(Hidden)シートもデータバインディングの対象から除外し、不要な演算を防止しました。
- `isBindingTargetSheet()`は、そのシートにBindingPathが存在するかを確認するロジックです。1つでもBindingPathが見つかれば、データバインディング対象のシートと判断します。
構成を整理すると、次のようになります。
|
[改善後の構成] すべてのシートを走査 -> データバインディングの要否を確認 -> 必要なシートにのみsetDataSource()を実行 |
|---|
6. ロード時間の改善結果
従来の実装では、詳細画面の全体の読み込み時間はテスト環境で約25~30秒でした。データバインディングが必要なシートだけを選別して処理するように変更したところ、約15~20秒まで短縮されました。
|
[改善前] 約25~30秒 ↓ [改善後] 約15~20秒(約40~50%短縮) |
|---|
テスト環境を基準に、読み込み時間を約40~50%短縮できることを確認しました。(※ユーザーのPCスペック、ブラウザー環境、Excelの構造によって差が生じる場合があります。)
ファイル容量を減らすだけでは大きく改善できなかった読み込みの遅延を、読み込み後に不要に実行されていた処理の範囲を縮小することで、大幅に改善できました。
7. まとめ
今回のパフォーマンス改善は、ファイルサイズやExcelの構造だけを原因と仮定せず、実際の読み込みプロセスを段階的に測定してボトルネックを特定した事例です。
最初は過去の経験をもとに、Excelの行・列の範囲が原因だと予想していましたが、ファイルサイズを縮小しても読み込み時間は大きく改善されませんでした。その後、読み込みプロセスごとの実行時間を測定したところ、`workbook.open()`よりも`setDataSource()`に大幅に時間がかかっていることを確認し、シートごとの測定を通じて、データバインディングが不要なシートでもこの処理が実行されていることを発見しました。
今回の改善により、テスト環境で初期読み込み時間を約25~30秒から15~20秒に短縮できました。ただし、最終的な読み込み時間にもさらなる改善の余地があると判断したため、今後はExcelの読み込み後に実行されるほかの処理まで範囲を広げて最適化する予定です。
複雑なExcel画面のパフォーマンス問題を確認する際は、ファイル容量だけでなく、実際の読み込みプロセスでどの処理にどれだけ時間がかかっているかを測定し、必要な処理だけを実行するように範囲を縮小することが重要だと確認できました。
参考資料
[SpreadJS Data Binding公式ドキュメント]
https://developer.mescius.com/spreadjs/docs/v19/features/binding
JY