Improve performance of body <> bodyEditorValue by making both mutations lazy

This commit is contained in:
Ben Gotow 2019-06-16 16:47:40 -05:00
parent 60c613fe7d
commit cfef571559

View file

@ -26,61 +26,86 @@ let DraftStore = null;
function hotwireDraftBodyState(draft: any, session: DraftEditingSession): MessageWithEditorState { function hotwireDraftBodyState(draft: any, session: DraftEditingSession): MessageWithEditorState {
// Populate the bodyEditorState and override the draft properties // Populate the bodyEditorState and override the draft properties
// so that they're kept in sync with minimal recomputation // so that they're kept in sync with minimal recomputation.
let _bodyHTMLCache = draft.body; let _bodyHTMLValue = draft.body;
let _bodyEditorState = null; let _bodyEditorValue = null;
draft.__bodyPropDescriptor = { draft.__bodyPropDescriptor = {
configurable: true, configurable: true,
get: function() { get: function() {
if (!_bodyHTMLCache) { if (!_bodyHTMLValue) {
console.log('building HTML body cache'); _bodyHTMLValue = convertToHTML(_bodyEditorValue);
_bodyHTMLCache = convertToHTML(_bodyEditorState);
} }
return _bodyHTMLCache; return _bodyHTMLValue;
}, },
set: function(inHTML) { set: function(inHTML) {
let nextValue = convertFromHTML(inHTML); if (_bodyHTMLValue === inHTML) return;
_bodyHTMLValue = inHTML;
if (session._mountedEditor) { if (session._mountedEditor) {
nextValue = session._mountedEditor // compute it now and apply it, preserving the document history
_bodyEditorValue = session._mountedEditor
.moveToRangeOfDocument() .moveToRangeOfDocument()
.delete()
.insertFragment(convertFromHTML(inHTML).document) .insertFragment(convertFromHTML(inHTML).document)
.moveToRangeOfDocument() .moveToRangeOfDocument()
.moveToStart() .moveToStart().value;
.deleteForward(1).value;
// occasionally inserting the new document adds a new line at the beginning of the value.
// It's unclaer why this happens...
const firstBlock = _bodyEditorValue.document.getBlocks().first();
if (firstBlock.text === '') {
_bodyEditorValue = session._mountedEditor.removeNodeByKey(firstBlock.key).value;
}
} else {
// compute it again when it's asked for
_bodyEditorValue = null;
} }
_bodyEditorState = nextValue;
}, },
}; };
draft.__bodyEditorStatePropDescriptor = { draft.__bodyEditorValuePropDescriptor = {
configurable: true, configurable: true,
get: function() { get: function() {
return _bodyEditorState; if (!_bodyEditorValue) {
}, _bodyEditorValue = convertFromHTML(_bodyHTMLValue);
set: function(next) {
if (_bodyEditorState !== next) {
_bodyHTMLCache = null;
} }
_bodyEditorState = next; return _bodyEditorValue;
},
set: function(inValue) {
if (_bodyEditorValue === inValue) return;
_bodyHTMLValue = null;
_bodyEditorValue = inValue;
}, },
}; };
Object.defineProperty(draft, 'body', draft.__bodyPropDescriptor); Object.defineProperty(draft, 'body', draft.__bodyPropDescriptor);
Object.defineProperty(draft, 'bodyEditorState', draft.__bodyEditorStatePropDescriptor); Object.defineProperty(draft, 'bodyEditorState', draft.__bodyEditorValuePropDescriptor);
draft.body = _bodyHTMLCache; draft.body = _bodyHTMLValue;
return draft as MessageWithEditorState; return draft as MessageWithEditorState;
} }
function fastCloneDraft(draft) { /**
* Note: This method is intended to return a new Mesasge object so that lazy people doing
* shallow equals get the correct basic behavior as the draft is modified in the session.
*
* However, this method does not deep clone array values (To:, etc.) and the hot-wired body
* and bodyEditorValue are linked through the same internal state. Changing the body of
* the cloned draft changes the body of the old draft too.
*
* At the moment these tradeoffs seem OK because we're really just trying to make
* "props.draft !== nextProps.draft" work.
*/
function fastCloneDraft(draft: MessageWithEditorState) {
const next = new Message({}); const next = new Message({});
for (const key of Object.getOwnPropertyNames(draft)) { for (const key of Object.getOwnPropertyNames(draft)) {
if (key === 'body' || key === 'bodyEditorState') continue; if (key === 'body' || key === 'bodyEditorState') continue;
next[key] = draft[key]; next[key] = draft[key];
} }
Object.defineProperty(next, 'body', (next as any).__bodyPropDescriptor); Object.defineProperty(next, 'body', (next as any).__bodyPropDescriptor);
Object.defineProperty(next, 'bodyEditorState', (next as any).__bodyEditorStatePropDescriptor); Object.defineProperty(next, 'bodyEditorState', (next as any).__bodyEditorValuePropDescriptor);
return next as MessageWithEditorState; return next as MessageWithEditorState;
} }
@ -342,7 +367,6 @@ export class DraftEditingSession extends MailspringStore {
// be made through the editing session and we don't want to overwrite the user's // be made through the editing session and we don't want to overwrite the user's
// work under any scenario. // work under any scenario.
const lockedFields = this.changes.dirtyFields(); const lockedFields = this.changes.dirtyFields();
let changed = false; let changed = false;
for (const [key] of Object.entries(Message.attributes)) { for (const [key] of Object.entries(Message.attributes)) {
if (key === 'headerMessageId') continue; if (key === 'headerMessageId') continue;
@ -356,6 +380,7 @@ export class DraftEditingSession extends MailspringStore {
} }
this._draft[key] = nextDraft[key]; this._draft[key] = nextDraft[key];
} }
if (changed) { if (changed) {
this.trigger(); this.trigger();
} }