MySQLに入ってる140列、10万行の巨大なテーブルの分析をするUIを作りたくて、普通にHTMLのテーブルを作ってみたらいつまでも表示が完了しなかった。試しにもっと小さい11列、6000行を表示してみたら表示完了まで1分40秒、メモリの使用量は1.5GByteになり、重すぎてスクロールはまともに動かなかった。8000行に増やすと5分待っても表示されず、3GByte以上使っていた。
そこで仮想スクロールを簡単に実現できるライブラリを探して試してみた。
要件
ライブラリに求める要件は以下の通り。
- node.jsの様なプラットフォームは使わず、JavaScriptのみで使えること
- dbは日々大きくなるので140列、20万行が扱えること
- 上下左右のスクロール時に、ヘッダを固定できること
- 列名やセルのクリックイベントを取れること
- セル(列)の色を変えられること
環境
サーバ側
- OS:Ubuntu 22.04.1 LTS(確認コマンド:lsb_release -a)
- apache:Apache/2.4.55 (Ubuntu)(確認コマンド:apache2 -V)
- python:3.10.12(確認コマンド:python -V)
- CPU:i5-2520M CPU @ 2.50GHz 4core(確認コマンド:cat /proc/cpuinfo)
- メモリ:8GByte
クライアント側
- OS:Windows10 Pro
- CPU: E3-1280 v3 @ 3.60GHz 4core
- メモリ:24GByte
- ブラウザ:chrome 120.0.6099.111(Official Build) (64 ビット)
- ブラウザ:brave バージョン: 1.61.101 Chromium: 120.0.6099.71(Official Build) (64 ビット)
ライブラリの一次選定
仮想スクロールのライブラリは沢山あるみたいで、評判の良さげなcheetah-gridを試してみたが、サンプルをサクッと動かせずさっさとあきらめた。
次に良さげに見えたのがSlickGrid。これはサンプルも簡単に動かせて、いろんなバリエーションの簡潔なサンプルが沢山あるのも良かった。サンプルの巨大なテーブルもサクサク動いてる。要件の1(node.jsとか使わずJavaScriptのみで動く)は満たしてるっぽいので、その他の要件も満たせるか試してみることにした。
サンプルを動かす
サンプルを試す方法は2つある。
1つは用意されたgithubのexsamples。ブラウザからそのまま動かせるのはありがたく、ここのUsing a filtered data view to drive the gridが6列、5万行でサクサク動くので評価に前向きになった。
もう1つは、git cloneしてローカルに設置して試す方法。
どっちも同じサンプルを動かせる。ローカルに置けば、ちょっとコードを変更して試すことができるので、要件を満たせるか試すには適してる。
ローカルで簡単に動かすには、apacheのDocumetRootの配下のフォルダでgit cloneして、
git clone https://github.com/6pac/SlickGrid.git
ブラウザから下記URLにアクセスするとgithubのExsamplesと同様のサンプルページにアクセスできる。
http://<サーバIP>/<展開したフォルダパス>/SlickGrid/examples/
要件を確認する
サンプルのBasic use with minimal configurationを使って、
- 20万行の動作確認
- スクロール時の左端のセルの固定
を試してみる。
簡単に動かしたかったので列数は変更せず、行数だけ増やしてみた。列数は要件よりかなり少ない(6列)けど、20万件は体感で500ms位で表示され、スクロールにストレスもない。10万件だと体感で100ms位で表示される。
左端のセルの固定も問題ない。評価する気が増してきた。
変更したスクリプト部のコードはこんな感じ。
<script>
var grid;
var columns = [
{id: "title", name: "Title", field: "title"},
{id: "duration", name: "Duration", field: "duration"},
{id: "%", name: "% Complete", field: "percentComplete", width: 90 },
{id: "start", name: "Start", field: "start"},
{id: "finish", name: "Finish", field: "finish"},
{id: "effort-driven", name: "Effort Driven", field: "effortDriven", width: 90 }
]; var options = {
enableCellNavigation: true,
enableColumnReorder: false,
frozenColumn: 0 // 2023/12/21 siba add
}; var data = [];
// for (var i = 0; i < 500; i++) {
// for (var i = 0; i < 1000000; i++) { // 2023/12/21 siba mod 500 to 1000,000.
for (var i = 0; i < 2000000; i++) { // 2023/12/21 siba mod 500 to 2000,000.
data[i] = {
title: "Task " + i,
duration: "5 days",
percentComplete: Math.round(Math.random() * 100),
start: "01/01/2009",
finish: "01/05/2009",
effortDriven: (i % 5 == 0)
};
} grid = new Slick.Grid("#myGrid", data, columns, options);
</script>
frozenColumnが左端から何セル分を固定するかで、セルの番号-1を指定する。
forの部分で、もともと500行だったところを20万行に変えた。
本番の環境作りを想定して、サンプルから独立した環境を作って要件通りのモックを作ってみる。新たに/var/www/html/test_table_slick2フォルダを作って、この中に必要な一式を入れて動かす。ライブラリ類はlibsの中に入れる。
# フォルダを作成
mkdir /var/www/html/test_table_slick2
cd /var/www/html/test_table_slick2
mkdir libs
cd libs
# SlickGlidを取得
git clone https://github.com/6pac/SlickGrid.git
# Sortable.jsを取得
git clone https://github.com/SortableJS/Sortable.git
140列、20万行にして、コードがちょっと長いけどセルのクリックイベント等を盛り込んだHTML(test_table_slick-03b.html)を作った。
<!DOCTYPE html>
<!-- 参考:https://tokkan.net/javascript/slickGrid1.html -->
<html>
<head>
<meta charset="UTF-8">
<title>SlickGrid test</title>
<script src="./libs/Sortable/Sortable.min.js"></script> <script src="./libs/SlickGrid/dist/browser/slick.core.js"></script>
<script src="./libs/SlickGrid/dist/browser/slick.interactions.js"></script>
<script src="./libs/SlickGrid/dist/browser/slick.grid.js"></script> <link rel="stylesheet" href="./libs/SlickGrid/dist/styles/css/slick-alpine-theme.css" type="text/css" />
</head> <body>
<div id="myGrid" class="slick-container" style="height:100vh;"></div>
<script>
var column_count = 140;
// var row_count = 50000; // 2sec
// var row_count = 100000; // 4sec
var row_count = 200000; // 7sec var columns = [];
for (var i = 0; i < column_count; i++) {
columns[columns.length] = { id: "title"+i.toString()+"", name: "Title"+i.toString()+"", field: "title"+i.toString()+"" }
}
// var options = {};
var options = {
// enableCellNavigation: true,
// enableColumnReorder: false,
frozenColumn: 0
};
var data = [];
for (var i = 0; i < row_count; i++) {
data[i] = {};
for (var j = 0; j < column_count; j++) {
if(j == 0){
data[i]["title"+j.toString()] = "row " + i.toString();
}
else{
data[i]["title"+j.toString()] = "abc" + j.toString();
}
}
} var grid = new Slick.Grid("#myGrid", data, columns, options);
// 参考:http://ja.uwenku.com/question/p-fskbhkdn-cm.html
grid.onDblClick.subscribe(function(e, args) {
console.log('duble clicked: ');
console.log(args);
var item = args.grid.getData()[args.row];
console.log(item); });
grid.onClick.subscribe(function(e, args) {
console.log('clicked: ');
console.log(args);
var item = args.grid.getData()[args.row];
console.log(item); });
// 参考:https://stackoverflow.com/questions/5703067/slickgrid-v2-0-column-name-id-does-not-follow-column-when-dragging
grid.onClick.subscribe(function(e,args) {
var allColumns=grid.getColumns();
console.log(allColumns[args.cell].name);
}); </script>
</body>
</html>
column_count、row_countで列数、行数を変更できるようにしといた。以下のサイトを参考にさせて頂きました。
- トッカンソフトウェアさんの「SlickGrid」
- uwenku.comの「Slickgrid、クリックイベントでグリッドアイテムを取得する方法は?」
- stackoverflow.comの「SlickGrid - Drag column header outside table」
- itecnote.comの「Get column name by index in SlickGrid」
動かしてる様子はこんな感じ。クリックイベントはconsoleに出力する。
表示完了までの時間は、
- 5万行:2秒
- 10万行:4秒
- 20万行:7秒
となった。10万や20万は常時使うわけではないので、この時間は許容範囲。
スクロールは20万行でもサクサクで問題ない。
セルのクリックイベントは課題が2つ見えた。
- 最上段の列名のセルをクリックしてもイベントを捕捉できない。
- ダブルクリック時は、クリックイベントも発火する。
ダブルクリックは使う予定が無いけど、定番のタイマーを使ってシングルクリックをキャンセルする等の処理をすれば良さそう。列名のセルのクリックイベントは、列名にボタン等を配置するプラグインがあるのでどうにかなるだろう。と思う。
セル、列、行の色の変更は試してないけど、サンプルを見るといろいろできそうなので、後で試すことにする。
表示までの時間の内訳
表示完了までの時間の殆どはデータ生成(本番だとデータの取得と加工)であって、表示時間自体にはあまりデータ量に依存しないのでは?と思ったので内訳をみてみた。
これは、chromeのdevtoolsのperformanceを使って、記録開始→CTRL+F5で再読み込み→表示完了で記録停止をして取ったデータ。
手描きの青線が全体の処理をやってる期間で、赤丸の中の紫のバーがSlickGridの処理。SlickGridの処理時間は164.21msとなってた。やっぱりデータ生成が支配的なのでSlickGridの性能は問題ではなく、高速化するならデータ生成を効率化するべきという感じの結果になった。
念のため、「var grid = new Slick.Grid("#myGrid", data, columns, options);」以下のスクリプトをコメントアウトしてSlickGridの処理を全て削除してみたけど、やっぱり表示完了まで7秒かかった。
本番では、MySQLに入ってるデータを使うので、サーバ側でのデータの読み出しとそのデータをクライアント側に送る時間(場合によってはSlickGridに渡すためのデータの加工時間も)が支配的になると予想される。
結果
SlickGrid自体のパフォーマンスには問題ない。20万件の表示完了までに7秒かかるが、巨大なテーブルを表示している自覚がある中での7秒なら固まったと思うほどではないし、頻繁に20万件を表示するわけではないし、そもそも本番ではデータの出元がMySQLになるので7秒は大きく変わる可能性がある。頻繁に20万件を表示するようなら設計から見直すべきと思ったり。
要件的には、時間の都合で以下が持ち越しになった。
- 列名のクリックイベントを取れること
- セル(列)の色を変えられること
1つ目は、サンプルの動きを見る限り、プラグインを参考にするか、プラグインを応用することでできそう。
2つ目もサンプルを見る限りできそうが気がする。これらはおいおい試してみる。