UI gets freeze when parsing large chunks of data even in web worker

62 views Asked by At

I am using Sheet JS to manipulate the contents of an Excel file that is being uploaded to the web app. When parsing a file that has more than 100,000 datasets, I used Web Worker to do it, but even though the UI still freezes, I used the Performance tab under the DevTools to track the parsing time, and it shows that I am getting frame drops.

Can you help me how to fix this?

consolidateWorkerV2:

export const consolidateWorkerV2 = () => {
  const workerCode = `
    /* Load standalone script from CDN */
    import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.1/package/xlsx.mjs";
    /* This callback will run once the main context sends a message */

async function getFirstRow(sheet, isInventory) {
  return await new Promise((resolve) => {
    for(let i = 0; i < 1000; i++) {

      if(isInventory){
        const sheetRange = "A4:Y5"
        const arr = XLSX.utils.sheet_to_json(sheet, { range: sheetRange });
        if(arr.length > 0) {
          resolve(arr);
          break;
        };
      } else {
        const sheetRange = "A1:Y" + i.toString();
        const arr = XLSX.utils.sheet_to_json(sheet, { range: sheetRange });
        if(arr.length > 0) {
          resolve(arr);
          break;
        };
      }
      
    };
  }); 
};

function getWorkbook(startRow, endRow, sheet, range, sheetName, headers){
  const chunkRange = { s: { r: startRow, c: range.s.c }, e: { r: endRow, c: range.e.c } };
  const sheetRange = XLSX.utils.encode_range(chunkRange);
  const SHEET_OPTIONS = { range: sheetRange, skipHeader: false, header: headers }
  const chunk = XLSX.utils.sheet_to_json(sheet, SHEET_OPTIONS);
  const chunkSheet = XLSX.utils.json_to_sheet(chunk);
  const workbook = { SheetNames: [sheetName], Sheets: { [sheetName]: chunkSheet } };
  return workbook;
};

self.addEventListener('message', async (e) => {
  try {
    const files = e.data.files; // Get the File object from the message
    for (const [index, item] of files.entries()) {
      const isFinished = index === files.length - 1;
      const file = item.file;
      const filename = item.file.name
      const data = await item.file.arrayBuffer();
      const readWorkbook = XLSX.read(data);
      
      for (const sheetName of readWorkbook.SheetNames) {
        const sheet = readWorkbook.Sheets[sheetName];
        const OPTIONS = { blankrows : false, defval: ''};
        const rowLength = XLSX.utils.sheet_to_row_object_array(sheet, OPTIONS);
        const totalRows = rowLength.length + 1;

        const inventoryBookPattern = /inventory[-_ ]?book/i;
        const isInventory = inventoryBookPattern.test(filename);
        const firstRow = await getFirstRow(sheet, isInventory);

        const range = XLSX.utils.decode_range(sheet["!ref"]);
        const headers = Object.keys(firstRow[0]);

        // Split the workbook into chunks and send each chunk separately
        if(totalRows > 1000){
          for (let startRow = range.s.r + 1; startRow <= range.e.r; startRow += 1000) {
            const MAXIMUM_ROW = 999;
            const endRow = Math.min(startRow + MAXIMUM_ROW, range.e.r);
            const workbook = getWorkbook(startRow, endRow, sheet, range, sheetName, headers);
            const isEndLoop = endRow >= range.e.r;
            postMessage({ workbook, filename, isFinished: isEndLoop });
          } 
        } else {
          const ADDITIONAL_ROWS = isInventory ? 4 : 1;
          const startRow = range.s.r + ADDITIONAL_ROWS;
          const MAXIMUM_ROW = totalRows - (isInventory ? 3 : 0);
          const endRow = Math.min(startRow + MAXIMUM_ROW, totalRows);
          const workbook = getWorkbook(startRow, endRow, sheet, range, sheetName, headers);
          postMessage({ workbook, filename, isFinished: true });
        }       
      }
    }
  } catch(e) {
    /* Pass the error message back */
    postMessage({ error: String(e.message || e).bold() });
  }
}, false);
`;

const blob = new Blob([workerCode], { type: "text/javascript" });
  const worker = new Worker(URL.createObjectURL(blob), { type: "module" });
  return worker;
};

consolidateReport:

async function consolidateReport(event) {
event.preventDefault();
try {
  if (!checkBoxFile.length || checkBoxFile.length === 1) {
    const errorMsg =
      checkBoxFile.length === 1
        ? "Need two files checked."
        : "No files checked.";

    throw new Error(errorMsg);
  } else if (!selectedDirectory){
    throw new Error("No directory selected.");
  }
  
  const files = checkBoxFile.filter((file) => file.isCheck === true);
  const worker = consolidateWorkerV2();
  
  if (files.find((file) => file.fileName === "Inventory Book")) {
    setDesiredDateTrigger(true);
    await waitForModalClose();
  }
  
  const newWorkbook = XLSX.utils.book_new();
  const [date, time] = getCurrentDateTime().split(", ");
  let fileName = namingConvention("Consolidated_Final_Report", date);
  const fileHandle = await selectedDirectory.getFileHandle(fileName + ".xlsx", { create: true });
  let filesCompleted = [];
  setIsLoading(true);

  worker.postMessage({ files });
  worker.onmessage = async (event) => {
    const { workbook, filename, isFinished, error } = event.data;
    if (isFinished) filesCompleted.push(filename);
    // if existing workbook has the same sheet name, then merge
    if (workbook) {
      const journalType = getJournalType(filename);
      try {
        if (newWorkbook.SheetNames.includes(journalType)) {
          const duplicateSheetName = workbook.SheetNames[0];
          const newWorksheet = newWorkbook.Sheets[journalType];
          const dupWorksheet = getDataRange(workbook.Sheets[duplicateSheetName]);
          const OPTIONS = { raw: false, defval: "", };
          const parsedDupWorksheet = XLSX.utils.sheet_to_json(dupWorksheet, OPTIONS);
          const updatedWorksheet = modifyWorksheet(dupWorksheet, parsedDupWorksheet, duplicateSheetName, journalType, reportDate);
          const newData = updatedWorksheet.map((row) => Object.values(row));
          XLSX.utils.sheet_add_aoa(newWorksheet, newData, { origin: -1, skipHeader: true });
        } else {
          // if no existing sheet name, then add new sheet
          for (const sheetName of workbook.SheetNames) {
            const worksheet = workbook.Sheets[sheetName];
            const worksheetData = getDataRange(worksheet);
            const parsedWorksheet = XLSX.utils.sheet_to_json(worksheet, { raw: false, defval: "", });
            const updatedWorksheet = modifyWorksheet(worksheetData, parsedWorksheet, sheetName, journalType, reportDate);
            const styledSheet = headerStyling(updatedWorksheet);
            XLSX.utils.book_append_sheet(newWorkbook, styledSheet, journalType);
          }
        }
      
        if (files.length === filesCompleted.length) {
          console.log("Consolidate started.");
          const excelBuffer = XLSX.write(newWorkbook, { bookType: "xlsx", type: "array", });
          const blob = new File([excelBuffer], fileName + ".xlsx");
          const writable = await fileHandle.createWritable();
          await writable.write(blob);
          await writable.close();
          worker.terminate();
          console.log("Consolidated report generated.");
          setIsLoading(false);
        } 
      } catch (error) {
        console.log("Error at consolidate: ", error);
        worker.terminate();
        setIsLoading(false);
      }
    } else if (error) {
      console.log("ERROR: ", error);
      worker.terminate();
      setIsLoading(false);
    }
  };
} catch (error) {
  setIsLoading(false);
  console.log("Error at sidebar: ", error);
  let errorMsg = error.message;
  const typeOfError =
    "An operation that depends on state cached in an interface object was made but the state had changed since it was read from disk";

  if (error.message.includes(typeOfError)) {
    errorMsg =
      "Please close the existing file before generating the report.";
  }
  toast.error(errorMsg, {
    toastId: "error-consolidate", // preventing to show multiple toast
    position: "top-center",
    autoClose: 1500,
    hideProgressBar: true,
    closeOnClick: false,
    pauseOnHover: false,
    draggable: false,
    theme: "light",
  });
 }
}
0

There are 0 answers