Skip to content

Commit

Permalink
xlsx: use TextDecoder and TextEncoder in browser
Browse files Browse the repository at this point in the history
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
  • Loading branch information
myfreeer committed Oct 5, 2020
1 parent ebb31f2 commit 99f48c3
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 4 deletions.
14 changes: 14 additions & 0 deletions lib/utils/browser-buffer-decode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const textDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');

function bufferToString(chunk) {
if (typeof chunk === 'string') {
return chunk;
}
if (textDecoder) {
return textDecoder.decode(chunk);
}
return chunk.toString();
}

exports.bufferToString = bufferToString;
15 changes: 15 additions & 0 deletions lib/utils/browser-buffer-encode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const textEncoder = typeof TextEncoder === 'undefined' ? null : new TextEncoder('utf-8');
const {Buffer} = require('buffer');

function stringToBuffer(str) {
if (typeof str !== 'string') {
return str;
}
if (textEncoder) {
return Buffer.from(textEncoder.encode(str).buffer);
}
return Buffer.from(str);
}

exports.stringToBuffer = stringToBuffer;
3 changes: 2 additions & 1 deletion lib/utils/parse-sax.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {SaxesParser} = require('saxes');
const {PassThrough} = require('readable-stream');
const {bufferToString} = require('./browser-buffer-decode');

module.exports = async function* (iterable) {
// TODO: Remove once node v8 is deprecated
Expand All @@ -17,7 +18,7 @@ module.exports = async function* (iterable) {
saxesParser.on('text', value => events.push({eventType: 'text', value}));
saxesParser.on('closetag', value => events.push({eventType: 'closetag', value}));
for await (const chunk of iterable) {
saxesParser.write(chunk.toString());
saxesParser.write(bufferToString(chunk));
// saxesParser.write and saxesParser.on() are synchronous,
// so we can only reach the below line once all events have been emitted
if (error) throw error;
Expand Down
6 changes: 6 additions & 0 deletions lib/utils/zip-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const events = require('events');
const JSZip = require('jszip');

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

// =============================================================================
// The ZipWriter class
Expand All @@ -25,6 +26,11 @@ class ZipWriter extends events.EventEmitter {
if (options.hasOwnProperty('base64') && options.base64) {
this.zip.file(options.name, data, {base64: true});
} else {
// https://www.npmjs.com/package/process
if (process.browser && typeof data === 'string') {
// use TextEncoder in browser
data = stringToBuffer(data);
}
this.zip.file(options.name, data);
}
}
Expand Down
23 changes: 20 additions & 3 deletions lib/xlsx/xlsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const StreamBuf = require('../utils/stream-buf');

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

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

Expand Down Expand Up @@ -283,11 +284,27 @@ class XLSX {
if (entryName[0] === '/') {
entryName = entryName.substr(1);
}
const stream = new PassThrough();
if (entryName.match(/xl\/media\//)) {
let stream;
if (entryName.match(/xl\/media\//) ||
// themes are not parsed as stream
entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/)) {
stream = new PassThrough();
stream.write(await entry.async('nodebuffer'));
} else {
const content = await entry.async('string');
// use object mode to avoid buffer-string convention
stream = new PassThrough({
writableObjectMode: true,
readableObjectMode: true,
});
let content;
// https://www.npmjs.com/package/process
if (process.browser) {
// running in browser, use TextDecoder if possible
content = bufferToString(await entry.async('nodebuffer'));
} else {
// running in node.js
content = await entry.async('string');
}
const chunkSize = 16 * 1024;
for (let i = 0; i < content.length; i += chunkSize) {
stream.write(content.substring(i, i + chunkSize));
Expand Down

0 comments on commit 99f48c3

Please sign in to comment.