기본적으로 UI 스레드에 등록된 사용자 정의 함수는 계산 워커와 UI 스레드 간의 왕복 처리를 통해 계산됩니다. supportCalcWorker()를 재정의하여 true를 반환하면 함수의 evaluate 로직이 직렬화되어 계산 워커 안에서 직접 실행되므로 왕복 처리 오버헤드를 줄일 수 있습니다.
이 데모는 동일한 두 SHA-256 해시 함수, 즉 기본 메인 스레드 계산을 사용하는 함수(HASH)와 계산 워커에서 직접 실행되는 함수(HASHINWORKER)를 1,000개의 수식 셀에 적용하여 성능 차이를 비교합니다.
제한 사항
모든 관련 코드는 evaluate 함수 안에 있어야 합니다.
SpreadJS는 customFunction.evaluate.toString()을 호출하여 함수 소스를 계산 워커로 보냅니다. 따라서 모든 관련 로직은 evaluate 또는 evaluateAsync 함수 자체 안에 정의되어야 합니다.
계산 워커에서는 통합 문서 및 워크시트 개체를 사용할 수 없습니다.
계산 워커 안에서는 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 "Calculating...";
};
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();
// Waits for the calc worker to be loaded
await spreadLeft.waitForAllCalculations();
await spreadRight.waitForAllCalculations();
setupSheet(sheetLeft, "HASHINWORKER", true);
setupSheet(sheetRight, "HASH", false);
// Initial calculation
var startLeft = performance.now();
sheetLeft.resumeCalcService();
await spreadLeft.waitForAllCalculations();
var timeLeft = (performance.now() - startLeft).toFixed(2);
sheetLeft.setValue(1, 0, "Calculation Time: " + timeLeft + " ms");
var startRight = performance.now();
sheetRight.resumeCalcService();
await spreadRight.waitForAllCalculations();
var timeRight = (performance.now() - startRight).toFixed(2);
sheetRight.setValue(1, 0, "Calculation Time: " + timeRight + " ms");
// Bind recalculate button click on each spread independently
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, "Calculating...");
var start = performance.now();
spread.calculate();
await spread.waitForAllCalculations();
var time = (performance.now() - start).toFixed(2);
sheet.setValue(1, 0, "Calculation Time: " + time + " ms");
isRecalculating = false;
});
}
bindRecalc(spreadLeft, sheetLeft);
bindRecalc(spreadRight, sheetRight);
}
var names = [
"Alice Johnson", "Bob Smith", "Charlie Brown", "Diana Prince", "Edward Norton",
"Fiona Apple", "George Martin", "Helen Troy", "Ivan Petrov", "Julia Roberts",
"Kevin Hart", "Laura Palmer", "Michael Scott", "Nancy Drew", "Oscar Wilde",
"Patricia Moore", "Quincy Adams", "Rachel Green", "Steven King", "Tina Turner"
];
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);
// Row 0: title
sheet.addSpan(headerRow, 0, 1, 4);
var title = isWorker ? "WITH supportCalcWorker()" : "WITHOUT 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);
// Row 1: calc time (cols 0-2) + recalculate button (col 3)
sheet.addSpan(timeRow, 0, 1, 3);
sheet.setValue(timeRow, 0, "Calculating...");
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);
// Recalculate button in D2
var btnCellType = new GC.Spread.Sheets.CellTypes.Button();
btnCellType.text("Recalculate");
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);
// Row 2: table headers
var headers = ["#", "Name", "Email", 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);
// Data rows
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 charset="utf-8" />
<meta name="spreadjs culture" content="ko-kr"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css">
<script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets-calc-worker/dist/gc.spread.sheets.calcworker.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/ko/purejs/node_modules/@mescius/spread-sheets-resources-ko/dist/gc.spread.sheets.resources.ko.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;
}