mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 15:56:10 +08:00
fix(composer): performance improvement in composer
Summary: The issue was that on every key stroke the whole composer, participants and all, were getting re-rendered. According to React.perf, the `TokenizingTextField`s were taking a very long time to render and never changing. This was fixed by adding a simple `shouldComponentUpdate` check. The composer also has several regions that only change when the `props` do. These are now cached. The cache reset when the `props` do. After all of that, rendering the whole composer still takes 20-40ms. If you're tying in the composer very quickly, text entry can approach that render time. This starts to stack multiple React rendering passes up and bogs the whole system down. Luckily, we can simply render the composer less frequently. Now, after changes are persisted to the `DraftStoreProxy`, we simply debounce the proxy `trigger`. The users don't see this because the native `contenteditable` field will update immediately. When the debounced proxy trigger fires, it will transparently update the view to the latest state. Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Subscribers: mg Differential Revision: https://phab.nylas.com/D1749
This commit is contained in:
parent
aeb83c3c50
commit
3f6257c009
|
@ -64,6 +64,10 @@ class ComposerView extends React.Component
|
|||
componentWillMount: =>
|
||||
@_prepareForDraft(@props.localId)
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
not Utils.isEqualReact(nextProps, @props) or
|
||||
not Utils.isEqualReact(nextState, @state)
|
||||
|
||||
componentDidMount: =>
|
||||
@_draftStoreUnlisten = DraftStore.listen @_onSendingStateChanged
|
||||
@_uploadUnlisten = FileUploadStore.listen @_onFileUploadStoreChange
|
||||
|
@ -104,6 +108,7 @@ class ComposerView extends React.Component
|
|||
@_focusOnUpdate = false
|
||||
|
||||
componentWillReceiveProps: (newProps) =>
|
||||
@_ignoreNextTrigger = false
|
||||
if newProps.localId isnt @props.localId
|
||||
# When we're given a new draft localId, we have to stop listening to our
|
||||
# current DraftStoreProxy, create a new one and listen to that. The simplest
|
||||
|
@ -426,6 +431,7 @@ class ComposerView extends React.Component
|
|||
@refs.contentBody.selectEnd()
|
||||
|
||||
_onDraftChanged: =>
|
||||
return if @_ignoreNextTrigger
|
||||
return unless @_proxy
|
||||
draft = @_proxy.draft()
|
||||
|
||||
|
@ -522,7 +528,24 @@ class ComposerView extends React.Component
|
|||
return unless @_proxy
|
||||
if @_getSelections().currentSelection?.atEndOfContent
|
||||
@props.onRequestScrollTo?(messageId: @_proxy.draft().id, location: "bottom")
|
||||
@_addToProxy(body: event.target.value)
|
||||
|
||||
# The body changes extremely frequently (on every key stroke). To keep
|
||||
# performance up, we don't want to trigger every single key stroke
|
||||
# since that will cause an entire composer re-render. We, however,
|
||||
# never want to lose any data, so we still add data to the proxy on
|
||||
# every keystroke.
|
||||
#
|
||||
# We want to use debounce instead of throttle because we don't want ot
|
||||
# trigger janky re-renders mid quick-type. Let's just do it at the end
|
||||
# when you're done typing and about to move onto something else.
|
||||
@_addToProxy({body: event.target.value}, {fromBodyChange: true})
|
||||
@_throttledTrigger ?= _.debounce =>
|
||||
@_ignoreNextTrigger = false
|
||||
@_proxy.trigger()
|
||||
, 100
|
||||
|
||||
@_throttledTrigger()
|
||||
return
|
||||
|
||||
_onChangeEditableMode: ({showQuotedText}) =>
|
||||
@setState showQuotedText: showQuotedText
|
||||
|
@ -534,6 +557,12 @@ class ComposerView extends React.Component
|
|||
|
||||
oldDraft = @_proxy.draft()
|
||||
return if _.all changes, (change, key) -> _.isEqual(change, oldDraft[key])
|
||||
|
||||
# Other extensions might want to hear about changes immediately. We
|
||||
# only need to prevent this view from re-rendering until we're done
|
||||
# throttling body changes.
|
||||
if source.fromBodyChange then @_ignoreNextTrigger = true
|
||||
|
||||
@_proxy.changes.add(changes)
|
||||
|
||||
@_saveToHistory(selections) unless source.fromUndoManager
|
||||
|
|
|
@ -124,7 +124,7 @@ class ContenteditableComponent extends React.Component
|
|||
# Note: Related to composer-view#_onClickComposeBody
|
||||
event.stopPropagation()
|
||||
|
||||
_onInput: (event) =>
|
||||
_onInput: =>
|
||||
@_dragging = false
|
||||
|
||||
@_runExtensionFilters()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
|
||||
{Contact, ContactStore} = require 'nylas-exports'
|
||||
{Utils, Contact, ContactStore} = require 'nylas-exports'
|
||||
{TokenizingTextField, Menu} = require 'nylas-component-kit'
|
||||
|
||||
class ParticipantsTextField extends React.Component
|
||||
|
@ -28,6 +28,10 @@ class ParticipantsTextField extends React.Component
|
|||
@defaultProps:
|
||||
visible: true
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
not Utils.isEqualReact(nextProps, @props) or
|
||||
not Utils.isEqualReact(nextState, @state)
|
||||
|
||||
render: =>
|
||||
classSet = {}
|
||||
classSet[@props.field] = true
|
||||
|
|
|
@ -31,10 +31,10 @@ class DraftChangeSet
|
|||
clearTimeout(@_timer) if @_timer
|
||||
@_timer = null
|
||||
|
||||
add: (changes, immediate) =>
|
||||
add: (changes, {immediate, silent}={}) =>
|
||||
@_pending = _.extend(@_pending, changes)
|
||||
@_pending['pristine'] = false
|
||||
@_onChange()
|
||||
@_onChange() unless silent
|
||||
if immediate
|
||||
@commit()
|
||||
else
|
||||
|
|
|
@ -433,7 +433,7 @@ class DraftStore
|
|||
@sessionForLocalId(messageLocalId).then (session) ->
|
||||
files = _.clone(session.draft().files) ? []
|
||||
files = _.reject files, (f) -> f.id is file.id
|
||||
session.changes.add({files}, true)
|
||||
session.changes.add({files}, immediate: true)
|
||||
|
||||
|
||||
module.exports = new DraftStore()
|
||||
|
|
Loading…
Reference in a new issue