Use ResizeObserver to monitor email content size for better performance

This commit is contained in:
Ben Gotow 2019-02-24 00:12:14 -08:00
parent 9f2b5217bc
commit 209ba58e3e
3 changed files with 66 additions and 104 deletions

View file

@ -24,6 +24,12 @@ export default class EmailFrame extends React.Component {
this._mounted = true; this._mounted = true;
this._writeContent(); this._writeContent();
this._unlisten = EmailFrameStylesStore.listen(this._writeContent); this._unlisten = EmailFrameStylesStore.listen(this._writeContent);
// Update the iframe's size whenever it's content size changes. Doing this
// with ResizeObserver is /so/ elegant compared to polling for it's height.
const iframeEl = ReactDOM.findDOMNode(this._iframeComponent);
this._iframeDocObserver = new ResizeObserver(this._onReevaluateContentSize);
this._iframeDocObserver.observe(iframeEl.contentDocument.firstElementChild);
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
@ -31,9 +37,9 @@ export default class EmailFrame extends React.Component {
const nextMessage = nextProps.message || {}; const nextMessage = nextProps.message || {};
return ( return (
message.id !== nextMessage.id ||
content !== nextProps.content || content !== nextProps.content ||
showQuotedText !== nextProps.showQuotedText || showQuotedText !== nextProps.showQuotedText ||
message.id !== nextMessage.id ||
!Utils.isEqualReact(message.pluginMetadata, nextMessage.pluginMetadata) !Utils.isEqualReact(message.pluginMetadata, nextMessage.pluginMetadata)
); );
} }
@ -44,9 +50,8 @@ export default class EmailFrame extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this._mounted = false; this._mounted = false;
if (this._unlisten) { if (this._iframeDocObserver) this._iframeDocObserver.disconnect();
this._unlisten(); if (this._unlisten) this._unlisten();
}
} }
_emailContent = () => { _emailContent = () => {
@ -60,71 +65,60 @@ export default class EmailFrame extends React.Component {
}; };
_writeContent = () => { _writeContent = () => {
const iframeNode = ReactDOM.findDOMNode(this._iframeComponent); const iframeEl = ReactDOM.findDOMNode(this._iframeComponent);
const doc = iframeNode.contentDocument; const doc = iframeEl.contentDocument;
if (!doc) { if (!doc) return;
return;
}
doc.open();
// NOTE: The iframe must have a modern DOCTYPE. The lack of this line // NOTE: The iframe must have a modern DOCTYPE. The lack of this line
// will cause some bizzare non-standards compliant rendering with the // will cause some bizzare non-standards compliant rendering with the
// message bodies. This is particularly felt with <table> elements use // message bodies. This is particularly felt with <table> elements use
// the `border-collapse: collapse` css property while setting a // the `border-collapse: collapse` css property while setting a
// `padding`. // `padding`.
doc.write('<!DOCTYPE html>');
const styles = EmailFrameStylesStore.styles(); const styles = EmailFrameStylesStore.styles();
if (styles) { doc.open();
doc.write(`<style>${styles}</style>`);
}
doc.write( doc.write(
`<div id='inbox-html-wrapper' class="${process.platform}">${this._emailContent()}</div>` `<!DOCTYPE html>` +
(styles ? `<style>${styles}</style>` : '') +
`<div id='inbox-html-wrapper' class="${process.platform}">${this._emailContent()}</div>`
); );
doc.close(); doc.close();
iframeNode.addEventListener('load', this._onLoad);
autolink(doc, { async: true });
adjustImages(doc);
for (const extension of MessageStore.extensions()) {
if (!extension.renderedMessageBodyIntoDocument) {
continue;
}
try {
extension.renderedMessageBodyIntoDocument({
document: doc,
message: this.props.message,
iframe: iframeNode,
});
} catch (e) {
AppEnv.reportError(e);
}
}
// Notify the EventedIFrame that we've replaced it's document (with `open`) // Notify the EventedIFrame that we've replaced it's document (with `open`)
// so it can attach event listeners again. // so it can attach event listeners again.
this._iframeComponent.didReplaceDocument(); this._iframeComponent.didReplaceDocument();
this._onMustRecalculateFrameHeight();
window.requestAnimationFrame(() => {
autolink(doc, { async: true });
adjustImages(doc);
for (const extension of MessageStore.extensions()) {
if (!extension.renderedMessageBodyIntoDocument) {
continue;
}
try {
extension.renderedMessageBodyIntoDocument({
document: doc,
message: this.props.message,
iframe: iframeEl,
});
} catch (e) {
AppEnv.reportError(e);
}
}
});
}; };
_onLoad = () => { _onReevaluateContentSize = () => {
const iframeNode = ReactDOM.findDOMNode(this._iframeComponent); const iframeEl = ReactDOM.findDOMNode(this._iframeComponent);
iframeNode.removeEventListener('load', this._onLoad); const doc = iframeEl && iframeEl.contentDocument;
this._setFrameHeight();
};
_onMustRecalculateFrameHeight = () => { // We must set the height to zero in order to get a valid scrollHeight
// if the document is wider and has a lower height now.
this._iframeComponent.setHeightQuietly(0); this._iframeComponent.setHeightQuietly(0);
this._lastComputedHeight = 0;
this._setFrameHeight();
};
_getFrameHeight = doc => {
let height = 0;
// If documentElement has a scroll height, prioritize that as height // If documentElement has a scroll height, prioritize that as height
// If not, fall back to body scroll height by setting it to auto // If not, fall back to body scroll height by setting it to auto
let height = 0;
if (doc && doc.documentElement && doc.documentElement.scrollHeight > 0) { if (doc && doc.documentElement && doc.documentElement.scrollHeight > 0) {
height = doc.documentElement.scrollHeight; height = doc.documentElement.scrollHeight;
} else if (doc && doc.body) { } else if (doc && doc.body) {
@ -134,59 +128,28 @@ export default class EmailFrame extends React.Component {
} }
height = doc.body.scrollHeight; height = doc.body.scrollHeight;
} }
return height;
this._iframeComponent.setHeightQuietly(height);
}; };
_setFrameHeight = () => { _onResize = () => {
if (!this._mounted) { const iframeEl = ReactDOM.findDOMNode(this._iframeComponent);
return; if (!iframeEl) return;
} this._iframeDocObserver.disconnect();
this._iframeDocObserver.observe(iframeEl.contentDocument.firstElementChild);
// Q: What's up with this holder?
// A: If you resize the window, or do something to trigger setFrameHeight
// on an already-loaded message view, all the heights go to zero for a brief
// second while the heights are recomputed. This causes the ScrollRegion to
// reset it's scrollTop to ~0 (the new combined heiht of all children).
// To prevent this, the holderNode holds the last computed height until
// the new height is computed.
const iframeNode = ReactDOM.findDOMNode(this._iframeComponent);
let height = this._getFrameHeight(iframeNode.contentDocument);
// Why 5px? Some emails have elements with a height of 100%, and then put
// tracking pixels beneath that. In these scenarios, the scrollHeight of the
// message is always <100% + 1px>, which leads us to resize them constantly.
// This is a hack, but I'm not sure of a better solution.
if (Math.abs(height - this._lastComputedHeight) > 5) {
this._iframeComponent.setHeightQuietly(height);
this._iframeHeightHolderEl.style.height = `${height}px`;
this._lastComputedHeight = height;
}
if (iframeNode.contentDocument.readyState !== 'complete') {
window.requestAnimationFrame(() => {
this._setFrameHeight();
});
}
}; };
render() { render() {
return ( return (
<div <EventedIFrame
className="iframe-container" searchable
ref={el => { onResize={this._onResize}
this._iframeHeightHolderEl = el; seamless="seamless"
style={{ height: 0 }}
ref={cm => {
this._iframeComponent = cm;
}} }}
style={{ height: this._lastComputedHeight }} />
>
<EventedIFrame
ref={cm => {
this._iframeComponent = cm;
}}
seamless="seamless"
searchable
onResize={this._onMustRecalculateFrameHeight}
/>
</div>
); );
} }
} }

View file

@ -483,16 +483,12 @@ body.platform-win32 {
margin: 0 auto; margin: 0 auto;
padding: 0 20px @spacing-standard 20px; padding: 0 20px @spacing-standard 20px;
.iframe-container { iframe {
margin-top: 10px; margin-top: 10px;
width: 100%; width: 100%;
border: 0;
iframe { padding: 0;
width: 100%; overflow: auto;
border: 0;
padding: 0;
overflow: auto;
}
} }
} }

View file

@ -87,8 +87,11 @@ class EventedIFrame extends React.Component {
} }
setHeightQuietly(height) { setHeightQuietly(height) {
this._ignoreNextResize = true; const el = ReactDOM.findDOMNode(this);
ReactDOM.findDOMNode(this).height = `${height}px`; if (el.style.height !== `${height}px`) {
this._ignoreNextResize = true;
el.style.height = `${height}px`;
}
} }
_onSearchableStoreChange = () => { _onSearchableStoreChange = () => {