How to detect when SpreadJS has fully completed rendering an Excel file?

Posted by: niraj.pandey on 28 August 2025, 8:10 am EST

  • Posted 28 August 2025, 8:10 am EST

    I’m implementing SpreadJS in a Vue 3 application and need to know when a spreadsheet has fully rendered. I am using SpreadJS to display multiple Excel files in a page, which can also be exported to PDF using headless Chromium via Puppeteer.

    Here is part of my current implementation:

    // Initialize SpreadJS
    const spread = new GC.Spread.Sheets.Workbook(containerElement, options);
    
    // Import Excel file
    spread.import(excelBlob, 
        function() {
            // Success callback
            console.log("Import succeeded");
            // But how do I know when the actual rendering is complete?
        },
        function(error) {
            // Error callback
            console.error("Import failed", error);
        },
        {
            fileType: GC.Spread.Sheets.FileType.excel
        }
    );

    While the success callback tells me the Excel data has been imported, it doesn’t guarantee that the spreadsheet has been fully rendered in the DOM. This is particularly problematic when working with complicated Excel files especially in headless browser environments (e.g., for PDF generation)

    with multiple SpreadJS instances on the same page. I have tried setting timeout of 5-20 seconds waiting to for render to complete but it is very unreliable.

    Are there specific events I can listen for after the import success callback which will ensure the sheet is fully rendered and not just loaded? Or is there a recommended pattern for detecting complete rendering?

  • Posted 29 August 2025, 4:58 am EST

    Hi,

    As I understand, you’re using Puppeteer to generate a PDF and want to ensure the rendering is fully completed before triggering PDF generation.

    The SpreadJS success callback only confirms that the workbook has loaded, but it doesn’t guarantee rendering completion. Since SpreadJS uses canvas-based rendering, the content is drawn continuously during scrolling and updates, and there’s no browser API to precisely detect when all rendering has finished. Additionally, Puppeteer can only capture what is currently visible in the DOM, so it won’t include all sheets or cells outside the viewport.

    For backend PDF generation, you could consider:

    .NET: https://developer.mescius.com/document-solutions/dot-net-excel-api

    Java: https://developer.mescius.com/document-solutions/java-excel-api

    If these options don’t fully meet your requirements, could you share more details about your exact use case with examples so we can better understand?

    Regards,

    Priyam

  • Posted 29 August 2025, 12:33 pm EST - Updated 29 August 2025, 2:18 pm EST

    Our Use Case

    Ensuring SpreadJS workbook is fully painted (canvas) before Puppeteer PDF capture in headless Chromium with multiple instances of SpreadJS

    We’re using SpreadJS inside a Vue 3 SPA and need a reliable way to know when a workbook / active sheet is fully rendered (painted to canvas) before triggering a headless Chromium PDF capture (Puppeteer 22.x) running in AWS Lambda.

    In browsers this works all the time, but in Lambda (headless Chromium) with several SpreadJS instances on the same page (3+ widgets) we see a race condition: some instances render fully and others never finish before we capture, even though the ‘import’ success callbacks run. We currently use a fixed timeout after ‘spread.resumePaint()’ but it’s unreliable.

    Below are environment details, simplified code excerpts, observed behaviour and specific questions.


    Environment / Context

    • Vue 3 single page app; SpreadJS loaded and used inside a dynamic Vue component (one SpreadJS workbook per widget).
    • Headless PDF export happens on AWS Lambda using headless Chromium via Puppeteer (Puppeteer 22.x).
    • SpreadJS is dynamically imported (to avoid bundling/loading all modules at start).
    • We use “lazy”/“incremental” patterns and try to optimise for headless PDF generation (disabled interactive features, suspend/resume painting, etc).
    • Typical problematic input: moderately complex Excel files (1–2 MB, 10–20 sheets).
    • Problem becomes much more likely when rendering 3+ instances concurrently on the same page.

    How we load SpreadJS (dynamic)

    const loadSpreadJSModules = async () => {
      if (GC) return GC; // Already loaded
    
      try {
        // Step 1: Import the core SpreadJS module first
        const spreadModule = await import("@mescius/spread-sheets");
    
        // Step 2: Load feature plugins first (Shapes before Charts)
        await import("@mescius/spread-sheets-shapes");
        await import("@mescius/spread-sheets-charts");
    
        // Step 3: Load the ExcelIO plugin last, as it depends on the others
        await import("@mescius/spread-sheets-io");
    
        GC = spreadModule;
        return GC;
      } catch (error) {
        console.error("Failed to load SpreadJS modules:", error);
        throw error;
      }
    };
    

    How we import files

    spread.import(
      blob,
      // Success callback
      function () {
        handleImportSuccess("Blob");
      },
      // Error callback
      function (error: any) {
        handleImportError(error);
      },
      // Options with performance settings
      importOptions,
    );
    

    Simplified ‘handleImportSuccess’ (current flow)

    const handleImportSuccess = async (method: string) => {
      try {
        // Resume calculation services
        try { spread.resumeCalcService(false); } catch (e) {}
    
        // Apply a bunch of settings (gridlines, autofilter, visibility cropping, etc)
        try { await applySpreadSettings(); } catch (e) { throw e; }
    
        // A font family update that wraps suspendPaint/resumePaint
        try { updateFontFamily(); } catch (e) {}
    
        // Hide scrollbars for PDF mode
        spread.options.showHorizontalScrollbar = false;
        spread.options.showVerticalScrollbar = false;
    
        // Resume painting (we previously suspended paint during import & settings)
        try { spread.resumePaint(); } catch (e) {}
    
        // Current heuristic: wait a fixed delay, then emit "render-complete"
        try {
          setTimeout(() => {
            emit("render-complete");
          }, 3000); // 3s delay (we've tried 5s/15s/25s)
        } catch (e) { throw e; }
      } catch (error) {
        console.error(error);
      }
    };
    

    Workbook options used on creation (we attempt to reduce overhead)

    const workbookOptions = {
      sheetCount: 1,
      calcOnDemand: true,
      incrementalCalculation: true,
      scrollByPixel: false,
      allowAutoExtendFilterRange: false,
      allowUndo: false,
      enableAccessibility: false,
      showDragDropTip: false,
      showDragFillTip: false,
      // For PDF mode we also set:
      allowUserResize: false,
      allowUserZoom: false,
      allowContextMenu: false,
      tabEditable: false,
      newTabVisible: false,
      allowSheetReorder: false,
      tabStripVisible: false,
      showResizeTip: false,
      highlightInvalidData: false,
    };
    

    Observed behaviour

    • In normal browsers:
      • import callback → apply settings → resumePaint → fixed 3s wait reliably produces a fully rendered canvas for many instances (3+).
    • In headless Chromium (Puppeteer on Lambda):
      • With 1–2 moderately complex files (1–2 MB, 10–20 sheets) rendering is usually OK.
      • With 3+ SpreadJS instances on the same page we sometimes complete only 1–2 instances; other instances never reach “render-complete” even though import success and settings logs exist for them.
      • Increasing timeouts (5s, 15s, 25s) did not fully solve the problem.
      • ‘requestAnimationFrame’-based approaches did not help in headless (no reliable rAF loop).
      • The stuck widgets’ debug logs typically stop after applying settings / starting import — they do not reach the “paint-resumed” / “render-complete” steps.

    Example debug timelines

    • Stuck widget (truncated):

      1. Workbook created
      2. Init SpreadJS
      3. Apply Settings
      4. Settings applied
      5. Importing file into SpreadJS

        — stops here, no “Import succeeded”, no “Paint resumed”, no “render-complete”
    • Successful widget:

      1. Workbook created
      2. Init SpreadJS
      3. Apply Settings
      4. Settings applied
      5. Importing file into SpreadJS
      6. Import succeeded
      7. Paint resumed
      8. Spreadsheet has been rendered
      9. render-complete

    What we’ve tried so far (to mitigate)

    • Suspended paint & calc during import, applied settings while suspended, then resumed paint & calc in a controlled order.
    • Tried “incremental” and “lazy” performance modes / workbook options to reduce CPU and memory pressure.
    • Tried rAF-based waits (does not work reliably in headless).
    • Tried fixed timeouts (5s, 15s, 25s) — inconsistent.
    • Attempted sequential rendering (queuing) logic in our app to render one widget at a time — reduced but did not eliminate the issue.

    Specific questions for SpreadJS support

    1. Does SpreadJS provide any internal event/callback that reliably indicates “canvas painting is complete” for the active sheet (beyond the ‘import’ success callback)? For instance, is there a documented lifecycle or API to detect final paint completion after calling ‘resumePaint()’?
    2. Is there a method to force synchronous painting or a “flush” call (e.g., ‘repaint()’, ‘flushRender’, ‘finishDrawing’) on the workbook or sheet API that ensures drawing operations are complete?
    3. Is there an internal workbook/sheet state or flag we can poll which reliably indicates that layout/drawing work is done (e.g., something exposed on ‘GC.Spread.Sheets.Workbook’ or the sheet object)?
    4. Are there recommended practices or configuration flags when running multiple SpreadJS instances in the same page (headless environment) to avoid race conditions or incomplete renders? Examples: forced sequential rendering, throttling concurrency, preferred workbook options for headless rendering, or memory/CPU settings.
    5. Is the interplay of ‘suspendPaint()’ / ‘resumePaint()’ and ‘suspendCalcService()’ / ‘resumeCalcService()’ known to be sensitive in headless Chromium? Is there a recommended ordering (resumeCalcService before resumePaint, or vice versa) that you guarantee will let canvases finish drawing?
    6. For our import + settings flow, what sequence do you recommend (import → resumeCalcService → apply settings → resumePaint → ???) to guarantee canvases are ready for capture? Any actions we should add after ‘resumePaint()’ to ensure completeness?
    7. Are there any known issues with Puppeteer 22.x / headless Chromium capturing SpreadJS canvases in Lambda? Are there recommended Chromium flags, Puppeteer settings, or Chromium versions that improve canvas rendering reliability for SpreadJS in headless mode?
    8. Is there a recommended SpreadJS version (or a patch) which provides better guarantees for headless rendering or emits a “render-complete” type event?
    9. If you recommend sequential rendering / queueing as the solution, what signal should the queue wait for? Is ‘file-loaded’ + ‘resumePaint()’ + X ms sufficient (and what X would you suggest for headless Lambda under moderate CPU), or is there a better internal signal to wait on?
    10. The Lambda Service has 8gb of RAM and 2 minute timeout.

    Thanks.

  • Posted 1 September 2025, 4:51 am EST

    Hi,

    • SpreadJS does not provide an official “paint finished / render-complete” event. Since SpreadJS renders on the canvas and updates whenever changes occur, there’s no API or property to confirm that rendering is complete.
    • The import success callback only guarantees that the model has loaded, not that the canvas has fully rendered.

    Below are reliable approaches, along with answers to your specific questions:

    Use SpreadJS PDF Export Instead of DOM Capture

    If the goal is to generate a PDF of the workbook, it’s best to use the SpreadJS PDF add-on. It exports directly from the workbook model, avoiding viewport/canvas limitations and timing issues.

    // after import success

    import “@mescius/spread-sheets-pdf”;

    const pdfOptions = new GC.Spread.Sheets.PDF.PdfSaveOptions();

    // pdfOptions.title = “My PDF”;

    // pdfOptions.author = “…”;

    // pdfOptions.printRange = “1-3”; // if needed

    spread.savePDF(

    (blob) => {

    // send blob to backend / S3 / response stream

    },

    (err) => console.error(“PDF export failed”, err),

    pdfOptions

    );

    Demo: https://developer.mescius.com/spreadjs/demos/features/pdf/basic-pdf/purejs

    For backend PDF generation, you can use DsExcel, which is fully compatible with SpreadJS and exports all sheets and cells, including those not visible in the viewport:

    .NET: https://developer.mescius.com/document-solutions/dot-net-excel-api

    Java: https://developer.mescius.com/document-solutions/java-excel-api

    Advantages

    • Exports all sheets, cells, and styles—not just what’s visible.
    • Deterministic, faster, and more reliable for automation/server scenarios.

    This is the recommended method, since Puppeteer only captures what’s visible in the viewport and cannot cover inactive sheets or off-screen cells.

    Answers to Specific Questions

    • “Canvas painting complete” event?

      No such event exists. Use SpreadJS PDF export or DsExcel for backend generation.

    • Force synchronous painting/flush?

      No public method guarantees that canvas rendering is finished.

    • Poll internal state/flag?

      None is exposed that reliably indicates layout/drawing completion.

    • Multiple instances (headless environment)?

      No specific flags available. The recommended solution is to use the built-in PDF export or DsExcel.

    • 5 & 6. Order of suspend/resume calc vs paint?

      No guaranteed order between resumeCalcService and resumePaint ensures complete rendering.

    • Known Puppeteer 22.x + Lambda issues?

      None specific to SpreadJS.

    • Future “render-complete” event?

      There is no such event planned at the moment. Since SJS does not support compatibility with third-party libraries, using the built-in PDF export remains the most reliable option.

    • Queuing strategy—what to wait on?

      There’s no reliable timing signal.

    In short, we recommend the following point.

    Prefer SpreadJS’s built-in PDF export or DsExcel ( server side pdf generations ) for reliable, full coverage with no rendering race conditions.

    Regards,

    Priyam

  • Posted 1 September 2025, 7:01 pm EST

    Thanks for the detailed response Priyam.

    While the PDF export looks good, our requirement is not to be able to export entire workbook but whatever is in the viewport along with other “widgets” which is a part of a wider dashboard. So we cannot really use the PDF exporter which exports just the Spreadsheet on its own. And from what you suggested, there doesn’t seem to be any real way to ascertain this. We even tried converting XLSX/XLSM to SJS but didn’t seem to notice much improvement in the rendering either in Lambda or lower spec devices.

    It works pretty much perfectly when there is only one instance of the plugin per page but as soon as we add a few instance along with complicated spreadsheets, especially in lower spec devices, it seems to freeze the page until the rendering is complete even with lazy mode.

    I suppose there isn’t much we can do about this then? Thanks for your time though.

  • Posted 2 September 2025, 1:36 am EST

    Hi Niraj,

    Thanks for clarifying your requirement. I now understand that you need a viewport capture of the spreadsheet in context with other widgets on the dashboard, which rules out the built-in PDF export.

    Unfortunately, SpreadJS doesn’t provide an explicit event or API to signal that rendering has fully completed, particularly when working with multiple concurrent instances in a headless environment. Since the rendering pipeline is canvas-based and incremental, it depends heavily on device/browser resources, and that’s why you see better performance on a single instance compared to several concurrent ones.

    So yes, at the moment, there’s no SpreadJS-native solution to guarantee canvas rendering completion for viewport capture via Puppeteer.

    Regards,

    Priyam

Need extra support?

Upgrade your support plan and get personal unlimited phone support with our customer engagement team

Learn More

Forum Channels