From 52e8e57e4ab2053ac79b2df2a4186e2b0ca7147d Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Wed, 23 Mar 2016 19:02:51 -0700 Subject: [PATCH] perf(autolinker): Use requestIdleCallback to fix hanging on large bodies --- .../message-list/lib/autolinker.es6 | 19 ++++++++++++++++--- .../message-list/lib/email-frame.jsx | 2 +- spec/stores/account-store-spec.coffee | 2 +- src/flux/stores/message-body-processor.es6 | 13 ++++++++++--- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/internal_packages/message-list/lib/autolinker.es6 b/internal_packages/message-list/lib/autolinker.es6 index 0bf64d1ae..b4b62388d 100644 --- a/internal_packages/message-list/lib/autolinker.es6 +++ b/internal_packages/message-list/lib/autolinker.es6 @@ -28,7 +28,7 @@ function _runOnTextNode(node, matchers) { } } -export function autolink(doc) { +export function autolink(doc, {async} = {}) { // Traverse the new DOM tree and make things that look like links clickable, // and ensure anything with an href has a title attribute. const textWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT); @@ -38,8 +38,21 @@ export function autolink(doc) { ['', RegExpUtils.urlRegex({matchEntireString: false})], ]; - while (textWalker.nextNode()) { - _runOnTextNode(textWalker.currentNode, matchers); + if (async) { + const fn = (deadline) => { + while (textWalker.nextNode()) { + _runOnTextNode(textWalker.currentNode, matchers); + if (deadline.timeRemaining() <= 0) { + window.requestIdleCallback(fn, {timeout: 500}); + return; + } + } + }; + window.requestIdleCallback(fn, {timeout: 500}); + } else { + while (textWalker.nextNode()) { + _runOnTextNode(textWalker.currentNode, matchers); + } } // Traverse the new DOM tree and make sure everything with an href has a title. diff --git a/internal_packages/message-list/lib/email-frame.jsx b/internal_packages/message-list/lib/email-frame.jsx index f43b3df1d..264c51ff8 100644 --- a/internal_packages/message-list/lib/email-frame.jsx +++ b/internal_packages/message-list/lib/email-frame.jsx @@ -65,7 +65,7 @@ export default class EmailFrame extends React.Component { doc.write(`
${this._emailContent()}
`); doc.close(); - autolink(doc); + autolink(doc, {async: true}); // Notify the EventedIFrame that we've replaced it's document (with `open`) // so it can attach event listeners again. diff --git a/spec/stores/account-store-spec.coffee b/spec/stores/account-store-spec.coffee index d0e212e24..a2c470a9b 100644 --- a/spec/stores/account-store-spec.coffee +++ b/spec/stores/account-store-spec.coffee @@ -55,7 +55,7 @@ describe "AccountStore", -> (new Account).fromJSON(@configAccounts[1]) ]) - it "should initialize tokens from config, if present, save them to keytar, and remove them from config", -> + it "should initialize tokens from config, if present, and save them to keytar", -> @configTokens = {'A': 'A-TOKEN'} @instance = new @constructor expect(@instance.tokenForAccountId('A')).toEqual('A-TOKEN') diff --git a/src/flux/stores/message-body-processor.es6 b/src/flux/stores/message-body-processor.es6 index 66455bbd4..c17d203be 100644 --- a/src/flux/stores/message-body-processor.es6 +++ b/src/flux/stores/message-body-processor.es6 @@ -35,6 +35,9 @@ class MessageBodyProcessor { return; } + // grab the old value + const oldOutput = this._recentlyProcessedD[changedKey].body; + // remove the message from the cache delete this._recentlyProcessedD[changedKey]; this._recentlyProcessedA = this._recentlyProcessedA.filter(({key}) => @@ -52,9 +55,13 @@ class MessageBodyProcessor { const updatedMessage = changedMessage.clone(); updatedMessage.body = updatedMessage.body || subscriptions[0].message.body; const output = this.retrieve(updatedMessage); - for (const subscription of subscriptions) { - subscription.callback(output); - subscription.message = updatedMessage; + + // only trigger if the output has really changed + if (output !== oldOutput) { + for (const subscription of subscriptions) { + subscription.callback(output); + subscription.message = updatedMessage; + } } } }