使用 Excel JavaScript API 优化性能

通过最小化进程、批处理函数和减小有效负载大小,编写更快、更具可缩放性的 Excel 加载项。 本文介绍模式、反模式和代码示例,以帮助你优化常见作。

快速改进

首先应用这些策略,以产生最大的直接影响。

  • Batch 加载和写入:组属性 load 调用,然后创建单个 context.sync()
  • 最小化对象创建:对块区域而不是许多单单元格区域进行作。
  • 在数组中写入数据,然后向目标区域分配一次。
  • 仅围绕大型更改暂停屏幕更新或计算。
  • 避免每次迭代 Excel.runcontext.sync() 内部循环。
  • 重复使用工作表、表和区域对象,而不是在循环内部重新查询。
  • 通过在分配之前进行分块或聚合,将有效负载保持在大小限制以下。

重要

建议使用 loadsync 调用可以解决许多性能问题。 有关以高效方式使用特定于应用程序的 API 的建议,请参阅 Office 外接程序的资源限制和性能优化 中的“使用特定于应用程序的 API 提高性能”部分。

暂时挂起 Excel 进程

Excel 执行对用户输入和外接程序作做出反应的后台任务。 暂停所选进程可以提高大型作的性能。

暂停计算

如果需要更新大范围 ((例如)赋值,然后重新计算依赖公式) 和不需要临时重新计算结果,请暂停计算,直到下一次 context.sync()

有关如何使用 suspendApiCalculationUntilNextSync() API 以便捷的方式暂停和重新激活计算的信息,请参阅应用程序对象参考文档。 以下代码演示了如何暂时挂起计算。

await Excel.run(async (context) => {
    let app = context.workbook.application;
    let sheet = context.workbook.worksheets.getItem("sheet1");
    let rangeToSet: Excel.Range;
    let rangeToGet: Excel.Range;
    app.load("calculationMode");
    await context.sync();
    // Calculation mode should be "Automatic" by default
    console.log(app.calculationMode);

    rangeToSet = sheet.getRange("A1:C1");
    rangeToSet.values = [[1, 2, "=SUM(A1:B1)"]];
    rangeToGet = sheet.getRange("A1:C1");
    rangeToGet.load("values");
    await context.sync();
    // Range value should be [1, 2, 3] now
    console.log(rangeToGet.values);

    // Suspending recalculation
    app.suspendApiCalculationUntilNextSync();
    rangeToSet = sheet.getRange("A1:B1");
    rangeToSet.values = [[10, 20]];
    rangeToGet = sheet.getRange("A1:C1");
    rangeToGet.load("values");
    app.load("calculationMode");
    await context.sync();
    // Range value should be [10, 20, 3] when we load the property, because calculation is suspended at that point
    console.log(rangeToGet.values);
    // Calculation mode should still be "Automatic" even with suspend recalculation
    console.log(app.calculationMode);

    rangeToGet.load("values");
    await context.sync();
    // Range value should be [10, 20, 30] when we load the property, because calculation is resumed after last sync
    console.log(rangeToGet.values);
});

仅暂停公式计算。 仍会重新生成任何已更改的引用。 例如,重命名工作表仍会更新公式中对该工作表的任何引用。

暂停屏幕更新

Excel 会在发生更改时显示这些更改。 对于大型迭代更新,请取消中间屏幕更新。 Application.suspendScreenUpdatingUntilNextSync() 暂停视觉对象更新,直到 下一个 context.sync() 或结束 Excel.run。 为用户提供反馈,例如状态文本或进度栏,因为 UI 在挂起期间显示为空闲状态。

注意

不要重复调用 suspendScreenUpdatingUntilNextSync (,例如循环) 。 重复调用将导致 Excel 窗口闪烁。

启用和禁用事件

有时可以通过禁用事件来提高性能。 使用事件文章中的代码示例展示了如何启用和禁用事件。

将数据导入表

将大型数据集直接导入 (例如重复调用 TableRowCollection.add())时,性能可能会降低。 请改用以下方法:

  1. 使用 将整个 2D 数组写入区域 range.values
  2. 在 () worksheet.tables.add() 的填充区域上创建表。

对于现有表,批量设置值 table.getDataBodyRange() 。 表会自动展开。

下面是此方法的一个示例:

await Excel.run(async (context) => {
    let sheet = context.workbook.worksheets.getItem("Sheet1");
    // Write the data into the range first.
    let range = sheet.getRange("A1:B3");
    range.values = [["Key", "Value"], ["A", 1], ["B", 2]];

    // Create the table over the range
    let table = sheet.tables.add('A1:B3', true);
    table.name = "Example";
    await context.sync();


    // Insert a new row to the table
    table.getDataBodyRange().getRowsBelow(1).values = [["C", 3]];
    // Change a existing row value
    table.getDataBodyRange().getRow(1).values = [["D", 4]];
    await context.sync();
});

注意

可以使用 Table.convertToRange() 方法将 Table 对象转换为 Range 对象,此做法非常方便。

有效负载大小限制最佳做法

Excel JavaScript API 对 API 调用具有大小限制。 Excel web 版将请求和响应限制为 5 MB。 如果超出此限制,API 将 RichAPI.Error 返回错误。 在所有平台上,一个范围限制为 500 万个单元格,用于获取作。 大范围通常超过这两个限制。

请求的有效负载大小组合在一起:

  • API 调用数。
  • 对象数,例如 Range 对象。
  • 要设置或获取的值的长度。

如果获得 RequestPayloadSizeLimitExceeded,请在拆分作之前应用以下策略来减小大小。

策略 1:将未更改的值移出循环

限制循环内部的进程以提高性能。 在以下代码示例中, context.workbook.worksheets.getActiveWorksheet() 可以移出循环, for 因为它不会在该循环中更改。

// DO NOT USE THIS CODE SAMPLE. This sample shows a poor performance strategy. 
async function run() {
  await Excel.run(async (context) => {
    let ranges = [];
    
    // This sample retrieves the worksheet every time the loop runs, which is bad for performance.
    for (let i = 0; i < 7500; i++) {
      let rangeByIndex = context.workbook.worksheets.getActiveWorksheet().getRangeByIndexes(i, 1, 1, 1);
    }    
    await context.sync();
  });
}

以下代码示例演示了类似的逻辑,但具有改进的策略。 该值 context.workbook.worksheets.getActiveWorksheet() 在循环之前检索,因为它不会更改。 仅应在循环中检索变化的值。

// This code sample shows a good performance strategy.
async function run() {
  await Excel.run(async (context) => {
    let ranges = [];
    // Retrieve the worksheet outside the loop.
    let worksheet = context.workbook.worksheets.getActiveWorksheet(); 

    // Only process the necessary values inside the loop.
    for (let i = 0; i < 7500; i++) {
      let rangeByIndex = worksheet.getRangeByIndexes(i, 1, 1, 1);
    }    
    await context.sync();
  });
}

策略 2:创建更少的范围对象

创建较少的范围对象以提高性能并减少有效负载大小。 下面有两种方法。

将每个范围数组拆分为多个数组

创建更少范围对象的一种方法是将每个范围数组拆分为多个数组,然后使用循环和新 context.sync() 调用处理每个新数组。

重要

只有在确认超过有效负载大小限制后,才使用此策略。 多个循环会减小每个有效负载请求的大小,但也会添加额外的 context.sync() 调用,并且可能会降低性能。

下面的代码示例尝试在单个循环中处理大范围数组,然后处理单个 context.sync() 调用。 在一次 context.sync() 调用中处理过多的范围值会导致有效负载请求大小超过 5MB 的限制。

// This code sample does not show a recommended strategy.
// Calling 10,000 rows would likely exceed the 5MB payload size limit in a real-world situation.
async function run() {
  await Excel.run(async (context) => {
    let worksheet = context.workbook.worksheets.getActiveWorksheet();
    
    // This sample attempts to process too many ranges at once. 
    for (let row = 1; row < 10000; row++) {
      let range = sheet.getRangeByIndexes(row, 1, 1, 1);
      range.values = [["1"]];
    }
    await context.sync(); 
  });
}

下面的代码示例显示了类似于上述代码示例的逻辑,但策略可避免超过 5MB 有效负载请求大小限制。 在以下代码示例中,区域在两个单独的循环中处理,每个循环后跟一个 context.sync() 调用。

// This code sample shows a strategy for reducing payload request size.
// However, using multiple loops and `context.sync()` calls negatively impacts performance.
// Only use this strategy if you've determined that you're exceeding the payload request limit.
async function run() {
  await Excel.run(async (context) => {
    let worksheet = context.workbook.worksheets.getActiveWorksheet();

    // Split the ranges into two loops, rows 1-5000 and then 5001-10000.
    for (let row = 1; row < 5000; row++) {
      let range = worksheet.getRangeByIndexes(row, 1, 1, 1);
      range.values = [["1"]];
    }
    // Sync after each loop. 
    await context.sync(); 
    
    for (let row = 5001; row < 10000; row++) {
      let range = worksheet.getRangeByIndexes(row, 1, 1, 1);
      range.values = [["1"]];
    }
    await context.sync(); 
  });
}

在数组中设置范围值

创建更少范围对象的另一种方法是创建数组,使用循环设置该数组中的所有数据,然后将数组值传递到某个区域。 这有利于性能和有效负载大小。 不是对循环中的每个范围调用 range.valuesrange.values 而是在循环外部调用一次。

下面的代码示例演示如何创建数组,在循环中 for 设置该数组的值,然后将数组值传递到循环外部的区域。

// This code sample shows a good performance strategy.
async function run() {
  await Excel.run(async (context) => {
    const worksheet = context.workbook.worksheets.getActiveWorksheet();    
    // Create an array.
    const array = new Array(10000);

    // Set the values of the array inside the loop.
    for (let i = 0; i < 10000; i++) {
      array[i] = [1];
    }

    // Pass the array values to a range outside the loop. 
    let range = worksheet.getRange("A1:A10000");
    range.values = array;
    await context.sync();
  });
}

后续步骤

另请参阅