Composer perf: Defer plugins & toolbar component rendering

This commit is contained in:
Ben Gotow 2018-02-13 23:37:51 -08:00
parent dc558c889e
commit 9d327cfd84
9 changed files with 102 additions and 50 deletions

View file

@ -89,6 +89,7 @@ export default class AccountContactField extends React.Component {
const { draft, session, accounts } = this.props;
return (
<InjectedComponentSet
deferred
className="dropdown-component"
matching={{ role: 'Composer:FromFieldComponents' }}
exposedProps={{

View file

@ -62,6 +62,7 @@ export default class ActionBarPlugins extends React.Component {
<span className={className}>
<div className="action-bar-cover" />
<InjectedComponentSet
deferred
className="composer-action-bar-plugins"
matching={{ role: ROLE }}
exposedProps={{

View file

@ -249,6 +249,7 @@ export default class ComposerView extends React.Component {
return (
<div className="composer-footer-region">
<InjectedComponentSet
deferred
matching={{ role: 'Composer:Footer' }}
exposedProps={{
draft: this.props.draft,
@ -298,6 +299,7 @@ export default class ComposerView extends React.Component {
_renderActionsWorkspaceRegion() {
return (
<InjectedComponentSet
deferred
matching={{ role: 'Composer:ActionBarWorkspace' }}
exposedProps={{
draft: this.props.draft,

View file

@ -71,6 +71,7 @@
font-size: 14px;
user-select: none;
position: relative;
.inner {
display: flex;
flex-direction: row;
@ -80,6 +81,7 @@
border-bottom: 1px solid @border-color-divider;
z-index: 2;
width: 100%;
min-height: 29px;
}
.divider {

View file

@ -1,27 +1,43 @@
import React from 'react';
export default class ComposerEditorToolbar extends React.Component {
constructor(props) {
super(props);
this.state = { visible: false };
}
componentDidMount() {
for (const el of document.querySelectorAll('.scroll-region-content')) {
el.addEventListener('scroll', this._onScroll);
}
this._mounted = true;
const parentScrollRegion = this._el.closest('.scroll-region-content');
if (parentScrollRegion) {
this._topClip = parentScrollRegion.getBoundingClientRect().top;
} else {
this._topClip = 0;
}
setTimeout(() => {
if (!this._mounted) return;
this.setState({ visible: true }, () => {
if (!this._mounted) return;
for (const el of document.querySelectorAll('.scroll-region-content')) {
el.addEventListener('scroll', this._onScroll);
}
this._el.style.height = `${this._innerEl.clientHeight}px`;
const parentScrollRegion = this._el.closest('.scroll-region-content');
if (parentScrollRegion) {
this._topClip = parentScrollRegion.getBoundingClientRect().top;
} else {
this._topClip = 0;
}
this._el.style.height = `${this._innerEl.clientHeight}px`;
});
}, 400);
}
componentDidUpdate() {
this._el.style.height = `${this._innerEl.clientHeight}px`;
this._onScroll();
if (this._el) {
this._el.style.height = `${this._innerEl.clientHeight}px`;
this._onScroll();
}
}
componentWillUnmount() {
this._mounted = false;
for (const el of document.querySelectorAll('.scroll-region-content')) {
el.removeEventListener('scroll', this._onScroll);
}
@ -44,6 +60,14 @@ export default class ComposerEditorToolbar extends React.Component {
const { value, onChange, plugins } = this.props;
let sectionItems = [];
if (!this.state.visible) {
return (
<div className="RichEditor-toolbar">
<div className="inner display-deferrable deferred" />
</div>
);
}
const pluginsWithToolbars = plugins.filter(
(p, idx) => p.toolbarComponents && p.toolbarComponents.length
);
@ -68,7 +92,7 @@ export default class ComposerEditorToolbar extends React.Component {
return (
<div ref={el => (this._el = el)} className="RichEditor-toolbar">
<div ref={el => (this._innerEl = el)} className="inner">
<div ref={el => (this._innerEl = el)} className="inner display-deferrable">
{sectionItems}
</div>
</div>

View file

@ -46,7 +46,9 @@ export default class InjectedComponentSet extends React.Component {
item rendered into the set.
- `containersRequired` (optional). Pass false to optionally remove the containers
placed around injected components to isolate them from the rest of the app.
- `deferred` (optiona). Pass true to render an empty space and fill in the injected
components after a few milliseconds. Useful for avoiding potential slowdowns in
getting core (composer) components onscreen.
- Any other props you provide, such as `direction`, `data-column`, etc.
will be applied to the {Flexbox} rendered by the InjectedComponentSet.
*/
@ -57,6 +59,7 @@ export default class InjectedComponentSet extends React.Component {
matchLimit: PropTypes.number,
exposedProps: PropTypes.object,
containersRequired: PropTypes.bool,
deferred: PropTypes.bool,
};
static defaultProps = {
@ -68,10 +71,23 @@ export default class InjectedComponentSet extends React.Component {
constructor(props, context) {
super(props, context);
this.state = this._getStateFromStores();
this.state = !props.deferred ? this._getStateFromStores() : { components: [], visible: false };
}
componentDidMount() {
this._mounted = true;
if (!this.props.deferred) {
this.listen();
} else {
setTimeout(() => {
this.setState(this._getStateFromStores());
this.listen();
}, 400);
}
}
listen() {
this._componentUnlistener = ComponentRegistry.listen(() =>
this.setState(this._getStateFromStores())
);
@ -84,6 +100,7 @@ export default class InjectedComponentSet extends React.Component {
}
componentWillUnmount() {
this._mounted = false;
if (this._componentUnlistener) {
this._componentUnlistener();
}
@ -118,13 +135,20 @@ export default class InjectedComponentSet extends React.Component {
});
if (visible) {
className += ' registered-region-visible';
className += ' injected-region-visible';
elements.unshift(
<InjectedComponentLabel key="_label" matching={matching} {...exposedProps} />
);
elements.push(<span key="_clear" style={{ clear: 'both' }} />);
}
if (this.props.deferred) {
className += ' display-deferrable';
if (!components.length) {
className += ' deferred';
}
}
return (
<Flexbox className={className} {...flexboxProps}>
{elements}

View file

@ -176,7 +176,7 @@ export default class InjectedComponent extends React.Component {
});
let className = this.props.className;
if (this.state.visible) {
className += ' registered-region-visible';
className += ' injected-region-visible';
}
const Component = this.state.component;

View file

@ -1,11 +1,6 @@
import { remote, clipboard } from 'electron';
import { React, PropTypes, Utils, Contact, ContactStore, RegExpUtils } from 'mailspring-exports';
import {
TokenizingTextField,
Menu,
InjectedComponent,
InjectedComponentSet,
} from 'mailspring-component-kit';
import { TokenizingTextField, Menu, InjectedComponentSet } from 'mailspring-component-kit';
const TokenRenderer = props => {
const { email, name } = props.token;
@ -214,35 +209,30 @@ export default class ParticipantsTextField extends React.Component {
// injected region feels out of place
return (
<div className={this.props.className}>
<InjectedComponent
<TokenizingTextField
ref={el => {
this._textfieldEl = el;
}}
matching={{ role: 'Composer:ParticipantsTextField' }}
fallback={TokenizingTextField}
requiredMethods={['focus']}
exposedProps={{
tokens: this.props.participants[this.props.field],
tokenKey: p => p.email,
tokenIsValid: p => ContactStore.isValidContact(p),
tokenRenderer: TokenRenderer,
onRequestCompletions: input => ContactStore.searchContacts(input),
shouldBreakOnKeydown: this._shouldBreakOnKeydown,
onInputTrySubmit: this._onInputTrySubmit,
completionNode: this._completionNode,
onAdd: this._add,
onRemove: this._remove,
onEdit: this._edit,
onEmptied: this.props.onEmptied,
onFocus: this.props.onFocus,
onTokenAction: this._onShowContextMenu,
menuClassSet: classSet,
menuPrompt: this.props.field,
field: this.props.field,
draft: this.props.draft,
headerMessageId: headerMessageId,
session: this.props.session,
}}
tokens={this.props.participants[this.props.field]}
tokenKey={p => p.email}
tokenIsValid={p => ContactStore.isValidContact(p)}
tokenRenderer={TokenRenderer}
onRequestCompletions={input => ContactStore.searchContacts(input)}
shouldBreakOnKeydown={this._shouldBreakOnKeydown}
onInputTrySubmit={this._onInputTrySubmit}
completionNode={this._completionNode}
onAdd={this._add}
onRemove={this._remove}
onEdit={this._edit}
onEmptied={this.props.onEmptied}
onFocus={this.props.onFocus}
onTokenAction={this._onShowContextMenu}
menuClassSet={classSet}
menuPrompt={this.props.field}
field={this.props.field}
draft={this.props.draft}
headerMessageId={headerMessageId}
session={this.props.session}
/>
</div>
);

View file

@ -332,7 +332,15 @@ body.is-blurred {
}
}
.registered-region-visible {
.display-deferrable {
opacity: 1;
transition: opacity 220ms;
&.deferred {
opacity: 0;
}
}
.injected-region-visible {
border: 1px dashed rgba(255, 0, 0, 0.5);
margin: 2px;
position: relative;