Skip to content

Commit 8eb2ccd

Browse files
committed
xlsx: use TextDecoder and TextEncoder in browser
Doing a profiling in chrome dev tools shows that the `Buffer.toString()` and `Buffer.from(string)` is using unexpected long cpu time. With the native TextDecoder and TextEncoder it can get much faster in browsers supporting it. On browsers not supporting TextDecoder, like Internet Explorer, this would fallback to original `Buffer.toString()` and `Buffer.from(string)`. This implements almost the same of exceljs#1458 in a non monkey-patching way covering xlsx only. Closes exceljs#1458 References: feross/buffer#268 feross/buffer#60 https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
1 parent ebb31f2 commit 8eb2ccd

File tree

5 files changed

+57
-4
lines changed

5 files changed

+57
-4
lines changed

lib/utils/browser-buffer-decode.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// eslint-disable-next-line node/no-unsupported-features/node-builtins
2+
const textDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');
3+
4+
function bufferToString(chunk) {
5+
if (typeof chunk === 'string') {
6+
return chunk;
7+
}
8+
if (textDecoder) {
9+
return textDecoder.decode(chunk);
10+
}
11+
return chunk.toString();
12+
}
13+
14+
exports.bufferToString = bufferToString;

lib/utils/browser-buffer-encode.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// eslint-disable-next-line node/no-unsupported-features/node-builtins
2+
const textEncoder = typeof TextEncoder === 'undefined' ? null : new TextEncoder('utf-8');
3+
const {Buffer} = require('buffer');
4+
5+
function stringToBuffer(str) {
6+
if (typeof str !== 'string') {
7+
return str;
8+
}
9+
if (textEncoder) {
10+
return Buffer.from(textEncoder.encode(str).buffer);
11+
}
12+
return Buffer.from(str);
13+
}
14+
15+
exports.stringToBuffer = stringToBuffer;

lib/utils/parse-sax.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const {SaxesParser} = require('saxes');
22
const {PassThrough} = require('readable-stream');
3+
const {bufferToString} = require('./browser-buffer-decode');
34

45
module.exports = async function* (iterable) {
56
// TODO: Remove once node v8 is deprecated
@@ -17,7 +18,7 @@ module.exports = async function* (iterable) {
1718
saxesParser.on('text', value => events.push({eventType: 'text', value}));
1819
saxesParser.on('closetag', value => events.push({eventType: 'closetag', value}));
1920
for await (const chunk of iterable) {
20-
saxesParser.write(chunk.toString());
21+
saxesParser.write(bufferToString(chunk));
2122
// saxesParser.write and saxesParser.on() are synchronous,
2223
// so we can only reach the below line once all events have been emitted
2324
if (error) throw error;

lib/utils/zip-stream.js

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const events = require('events');
22
const JSZip = require('jszip');
33

44
const StreamBuf = require('./stream-buf');
5+
const {stringToBuffer} = require('./browser-buffer-encode');
56

67
// =============================================================================
78
// The ZipWriter class
@@ -25,6 +26,11 @@ class ZipWriter extends events.EventEmitter {
2526
if (options.hasOwnProperty('base64') && options.base64) {
2627
this.zip.file(options.name, data, {base64: true});
2728
} else {
29+
// https://www.npmjs.com/package/process
30+
if (process.browser && typeof data === 'string') {
31+
// use TextEncoder in browser
32+
data = stringToBuffer(data);
33+
}
2834
this.zip.file(options.name, data);
2935
}
3036
}

lib/xlsx/xlsx.js

+20-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const StreamBuf = require('../utils/stream-buf');
66

77
const utils = require('../utils/utils');
88
const XmlStream = require('../utils/xml-stream');
9+
const {bufferToString} = require('../utils/browser-buffer-decode');
910

1011
const StylesXform = require('./xform/style/styles-xform');
1112

@@ -283,11 +284,27 @@ class XLSX {
283284
if (entryName[0] === '/') {
284285
entryName = entryName.substr(1);
285286
}
286-
const stream = new PassThrough();
287-
if (entryName.match(/xl\/media\//)) {
287+
let stream;
288+
if (entryName.match(/xl\/media\//) ||
289+
// themes are not parsed as stream
290+
entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/)) {
291+
stream = new PassThrough();
288292
stream.write(await entry.async('nodebuffer'));
289293
} else {
290-
const content = await entry.async('string');
294+
// use object mode to avoid buffer-string convention
295+
stream = new PassThrough({
296+
writableObjectMode: true,
297+
readableObjectMode: true,
298+
});
299+
let content;
300+
// https://www.npmjs.com/package/process
301+
if (process.browser) {
302+
// running in node.js
303+
content = await entry.async('string');
304+
} else {
305+
// running in browser, use TextDecoder if possible
306+
content = bufferToString(await entry.async('nodebuffer'));
307+
}
291308
const chunkSize = 16 * 1024;
292309
for (let i = 0; i < content.length; i += chunkSize) {
293310
stream.write(content.substring(i, i + chunkSize));

0 commit comments

Comments
 (0)