用 Web Worker 解析大 CSV:分块读取、进度回传与内存控制
CSV 看起来简单,但文件一大,两个问题立刻出现:解析本身占满主线程,页面卡死;一次性把内容读进内存,几十兆还好,几百兆就容易崩。把解析放进 Web Worker,再配合分块读取,能同时解决这两个问题。
一、为什么一定要用 Worker
主线程负责渲染和交互,解析 CSV 是纯计算。哪怕算法很快,几百万次字符串切分也会让主线程长时间无法响应。Web Worker 跑在独立线程里,解析期间页面照常滚动、按钮照常点击。
const worker = new Worker('./csv-worker.js');
worker.postMessage({ type: 'start', file });
worker.onmessage = (e) => {
if (e.data.type === 'progress') setProgress(e.data.value);
if (e.data.type === 'done') setRows(e.data.rows);
};
二、分块读取,而不是一次读完
File 对象本身支持 slice,可以按固定大小分块读取。做法是每次切 4MB 左右,用 FileReader 或 Blob.text() 读成字符串,再交给解析器。块与块之间可能把一行从中间截断,所以要保留「上一块最后一行未完成的部分」:
const CHUNK = 4 * 1024 * 1024;
let rest = '';
let offset = 0;
while (offset < file.size) {
const blob = file.slice(offset, offset + CHUNK);
const text = rest + await blob.text();
const lines = text.split('\n');
rest = lines.pop() ?? '';
parseLines(lines);
offset += CHUNK;
postProgress(offset / file.size);
}
这里的关键是 lines.pop():最后一行可能不完整,必须留到下一块拼接,否则会出现字段错位或半行数据。
三、解析器要处理引号和换行
直接用 split(',') 只适用于最简单的文件。真实 CSV 里字段可能被双引号包裹,引号内还可能包含逗号和换行。一个够用的状态机是逐字符扫描:
function parseLine(line) {
const out = [];
let cur = '', quoted = false;
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (ch === '"') {
if (quoted && line[i + 1] === '"') { cur += '"'; i++; }
else quoted = !quoted;
} else if (ch === ',' && !quoted) {
out.push(cur); cur = '';
} else {
cur += ch;
}
}
out.push(cur);
return out;
}
如果字段内允许换行,就不能按行切分,而要按字符流推进状态机。这时分块读取的意义更大,因为状态可以跨块保留。
四、进度回传要节流
每读一块就 postMessage 一次没问题,但如果每解析一行就发一次消息,主线程反而会被消息淹没。按块或按时间节流,比如每 100ms 最多发一次:
let last = 0;
function postProgress(v) {
const now = performance.now();
if (now - last < 100 && v < 1) return;
last = now;
self.postMessage({ type: 'progress', value: v });
}
五、内存控制:别把整张表都留着
如果只是预览前 N 行,解析到够用就可以停止,后面的块不必读。如果确实要保留全部数据,优先用列式存储:把每一列放进独立的数组,比几百万个小对象省很多内存。
const cols = { id: [], name: [], amount: [] };
for (const row of rows) {
cols.id.push(row[0]);
cols.name.push(row[1]);
cols.amount.push(Number(row[2]));
}
数值列还可以进一步用 Float64Array 或 Int32Array,前提是能确定取值范围和缺失值表示方式。
六、几个容易踩的坑
编码。国内导出的 CSV 常见 GBK,直接按 UTF-8 读会乱码。可以先读前几个字节判断 BOM,或者让用户选择编码,再用 TextDecoder 解码。
Worker 里的 DOM。Worker 拿不到 document,所有界面更新都要通过 postMessage 回到主线程做,不要在 Worker 里试图操作元素。
取消任务。用户切走页面时要能中止。用一个标志位在每块开始时检查,或者直接 terminate Worker,避免后台继续吃 CPU。
七、小结
大 CSV 解析的关键不是解析算法本身,而是把工作拆开:Worker 负责计算,分块负责控制内存峰值,状态机负责处理引号和换行,节流负责让进度条不拖慢界面。四件事各归各位,几百兆的文件也能平稳处理。
- 点赞
- 收藏
- 关注作者
评论(0)