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",
});
}
}