Support Custom Function in Calc Worker

Custom functions directly inside the Calc Worker by overriding the supportCalcWorker() method to return true. This eliminates the round-trip to the UI thread for evaluation and can significantly improve performance.

By default, custom functions registered on the UI thread are evaluated via a round-trip between the Calc Worker and the UI thread. You can override supportCalcWorker() to return true so the function's evaluate logic is serialized and executed directly inside the Calc Worker, avoiding the round-trip overhead. This demo compares two identical SHA-256 hashing functions — one using the default main thread evaluate (HASH) and one running directly in the Calc Worker (HASHINWORKER) — across 1,000 formula cells to show the performance difference. Limitations All related code must live inside the evaluate function. SpreadJS calls customFunction.evaluate.toString() and sends the function source to the Calc Worker. This means all relevant logic must be defined inside the evaluate (or evaluateAsync) function itself. Workbook and Worksheet objects are not available in the Calc Worker. Inside the Calc Worker, there are no exposed objects or APIs such as GC.Spread.Sheets, spread.calculate, or spread.suspendCalcService. Custom functions can only use the evaluation context and Web Worker built-in APIs.
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="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css"> <script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-calc-worker/dist/gc.spread.sheets.calcworker.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; }