既定では、UI スレッドで登録されたカスタム関数は、計算ワーカーと UI スレッド間の往復処理によって評価されます。supportCalcWorker() をオーバーライドして true を返すことで、関数の evaluate ロジックをシリアライズし、計算ワーカー内で直接実行して往復処理のオーバーヘッドを回避できます。
このデモでは、同一の SHA-256 ハッシュ関数を 2 つ比較します。1 つは既定のメインスレッド評価(HASH)を使用し、もう 1 つは Calc Worker 内で直接実行(HASHINWORKER)します。1,000 個の数式セルで実行して、パフォーマンスの違いを示します。
制限事項
関連するすべてのコードは evaluate 関数内に配置する必要があります。
SpreadJS は customFunction.evaluate.toString() を呼び出し、関数ソースを 計算ワーカーに送信します。そのため、関連するすべてのロジックは evaluate(または evaluateAsync)関数自体の内部で定義する必要があります。
計算ワーカーでは Workbook オブジェクトと Worksheet オブジェクトは使用できません。
計算ワーカー内では、GC.Spread.Sheets、spread.calculate、spread.suspendCalcService などのオブジェクトや API は公開されていません。カスタム関数で使用できるのは、評価コンテキストと Web Worker の組み込み API のみです。
window.onload = function () {
var spreadLeft = new GC.Spread.Sheets.Workbook(document.getElementById("ssLeft"), { sheetCount: 1 });
var spreadRight = new GC.Spread.Sheets.Workbook(document.getElementById("ssRight"), { sheetCount: 1 });
spreadLeft.options.incrementalCalculation = true;
spreadRight.options.incrementalCalculation = true;
initSpread(spreadLeft, spreadRight);
};
async function initSpread(spreadLeft, spreadRight) {
function HashFunction() {
}
HashFunction.prototype = new GC.Spread.CalcEngine.Functions.AsyncFunction("HASH", 1, 1);
HashFunction.prototype.defaultValue = function () {
return "計算中...";
};
HashFunction.prototype.evaluateAsync = function (context, value) {
var text = value == null ? "" : String(value);
var encoder = new TextEncoder();
var data = encoder.encode(text);
crypto.subtle.digest("SHA-256", data).then(function (hashBuffer) {
var hashArray = Array.from(new Uint8Array(hashBuffer));
var hashHex = hashArray.map(function (b) { return b.toString(16).padStart(2, "0"); }).join("");
context.setAsyncResult(hashHex.substring(0, 6));
});
};
function HashInWorkerFunction() {
}
HashInWorkerFunction.prototype = new HashFunction();
HashInWorkerFunction.prototype.name = "HASHINWORKER";
HashInWorkerFunction.prototype.supportCalcWorker = function () {
return true;
};
GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction("HASH", new HashFunction());
GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction("HASHINWORKER", new HashInWorkerFunction());
var sheetLeft = spreadLeft.getActiveSheet();
var sheetRight = spreadRight.getActiveSheet();
// Calc Worker の読み込み完了を待機します
await spreadLeft.waitForAllCalculations();
await spreadRight.waitForAllCalculations();
setupSheet(sheetLeft, "HASHINWORKER", true);
setupSheet(sheetRight, "HASH", false);
// 初回計算
var startLeft = performance.now();
sheetLeft.resumeCalcService();
await spreadLeft.waitForAllCalculations();
var timeLeft = (performance.now() - startLeft).toFixed(2);
sheetLeft.setValue(1, 0, "計算時間: " + timeLeft + " ms");
var startRight = performance.now();
sheetRight.resumeCalcService();
await spreadRight.waitForAllCalculations();
var timeRight = (performance.now() - startRight).toFixed(2);
sheetRight.setValue(1, 0, "計算時間: " + timeRight + " ms");
// 各 Spread の再計算ボタンを個別にバインドします
function bindRecalc(spread, sheet) {
var isRecalculating = false;
spread.bind(GC.Spread.Sheets.Events.ButtonClicked, async function () {
if (isRecalculating) {
return;
}
isRecalculating = true;
sheet.setValue(1, 0, "計算中...");
var start = performance.now();
spread.calculate();
await spread.waitForAllCalculations();
var time = (performance.now() - start).toFixed(2);
sheet.setValue(1, 0, "計算時間: " + time + " ms");
isRecalculating = false;
});
}
bindRecalc(spreadLeft, sheetLeft);
bindRecalc(spreadRight, sheetRight);
}
var names = [
"佐藤 太郎", "鈴木 花子", "田中 一郎", "高橋 美咲", "伊藤 健太",
"渡辺 さくら", "山本 大輔", "中村 葵", "小林 翔", "加藤 優子",
"吉田 陽菜", "山田 直樹", "佐々木 真央", "山口 拓也", "松本 結衣",
"井上 亮", "木村 彩", "林 俊介", "清水 愛", "森 悠斗"
];
var emails = [
"alice@example.com", "bob@example.com", "charlie@example.com", "diana@example.com", "edward@example.com",
"fiona@example.com", "george@example.com", "helen@example.com", "ivan@example.com", "julia@example.com",
"kevin@example.com", "laura@example.com", "michael@example.com", "nancy@example.com", "oscar@example.com",
"patricia@example.com", "quincy@example.com", "rachel@example.com", "steven@example.com", "tina@example.com"
];
function setupSheet(sheet, funcName, isWorker) {
sheet.suspendPaint();
sheet.suspendCalcService();
sheet.options.allowCellOverflow = true;
var headerColor = isWorker ? "#51cf66" : "#ff6b6b";
var headerForeColor = "#ffffff";
var lightBg = isWorker ? "#d3f9d8" : "#ffe0e0";
var dataRows = 1000;
var headerRow = 0;
var timeRow = 1;
var tableHeaderRow = 2;
var dataStartRow = 3;
sheet.setRowCount(dataStartRow + dataRows);
sheet.setColumnCount(4);
sheet.setColumnWidth(0, 40);
sheet.setColumnWidth(1, 150);
sheet.setColumnWidth(2, 170);
sheet.setColumnWidth(3, 120);
// 0 行目: タイトル
sheet.addSpan(headerRow, 0, 1, 4);
var title = isWorker ? "supportCalcWorker() あり" : "supportCalcWorker() なし";
sheet.setValue(headerRow, 0, title);
sheet.getCell(headerRow, 0).backColor(headerColor).foreColor(headerForeColor).font("bold 14px Arial")
.hAlign(GC.Spread.Sheets.HorizontalAlign.center).vAlign(GC.Spread.Sheets.VerticalAlign.center);
sheet.setRowHeight(headerRow, 36);
// 1 行目: 計算時間(0-2 列)と再計算ボタン(3 列)
sheet.addSpan(timeRow, 0, 1, 3);
sheet.setValue(timeRow, 0, "計算中...");
sheet.getCell(timeRow, 0).backColor(lightBg).font("bold 16px Consolas")
.hAlign(GC.Spread.Sheets.HorizontalAlign.center).vAlign(GC.Spread.Sheets.VerticalAlign.center);
sheet.setRowHeight(timeRow, 30);
// D2 の再計算ボタン
var btnCellType = new GC.Spread.Sheets.CellTypes.Button();
btnCellType.text("再計算");
btnCellType.buttonBackColor(lightBg);
sheet.setCellType(timeRow, 3, btnCellType);
sheet.getCell(timeRow, 3).backColor(lightBg)
.hAlign(GC.Spread.Sheets.HorizontalAlign.center).vAlign(GC.Spread.Sheets.VerticalAlign.center);
// 2 行目: テーブルヘッダー
var headers = ["#", "名前", "メール", funcName + "()"];
for (var c = 0; c < headers.length; c++) {
sheet.setValue(tableHeaderRow, c, headers[c]);
sheet.getCell(tableHeaderRow, c).backColor("#f1f3f5").font("bold 12px Arial")
.hAlign(GC.Spread.Sheets.HorizontalAlign.center);
}
sheet.setRowHeight(tableHeaderRow, 26);
// データ行
for (var i = 0; i < dataRows; i++) {
var row = dataStartRow + i;
sheet.setValue(row, 0, i + 1);
sheet.setValue(row, 1, names[i % names.length]);
sheet.setValue(row, 2, emails[i % emails.length]);
sheet.setFormula(row, 3, "=" + funcName + "(B" + (row + 1) + "&C" + (row + 1) + ")");
if (i % 2 === 1) {
sheet.getRange(row, 0, 1, 4).backColor("#fafafa");
}
}
sheet.resumePaint();
}
<!doctype html>
<html style="height:100%;font-size:14px;">
<head>
<meta name="spreadjs culture" content="ja-jp" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/ja/purejs/node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css">
<script src="$DEMOROOT$/ja/purejs/node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/ja/purejs/node_modules/@mescius/spread-sheets-calc-worker/dist/gc.spread.sheets.calcworker.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/ja/purejs/node_modules/@mescius/spread-sheets-resources-ja/dist/gc.spread.sheets.resources.ja.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script>
<script src="app.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div class="sample-tutorial">
<div class="spread-panel">
<div id="ssLeft" class="sample-spreadsheets"></div>
</div>
<div class="spread-panel">
<div id="ssRight" class="sample-spreadsheets"></div>
</div>
</div>
</body>
</html>
body {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
}
.sample-tutorial {
position: relative;
height: 100%;
overflow: hidden;
display: flex;
}
.spread-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.spread-panel:first-child {
border-right: 2px solid #e0e0e0;
}
.sample-spreadsheets {
flex: 1;
overflow: hidden;
}