Dashboard performance improvements (#1671)

* dashboard: reduce rerendering of FileItems

* dashboard: move pure HOC to util file

* dashboard: memoize arrays passed to child elements too

* dashboard: fix memoize-one import when bundler used `module` key
This commit is contained in:
Renée Kooi 2019-07-02 17:25:53 +02:00 committed by Artur Paikin
parent 1d8ee14ead
commit fcfdf64d7e
6 changed files with 115 additions and 47 deletions

10
package-lock.json generated
View file

@ -14652,6 +14652,11 @@
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
},
"is-shallow-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-shallow-equal/-/is-shallow-equal-1.0.1.tgz",
"integrity": "sha512-lq5RvK+85Hs5J3p4oA4256M1FEffzmI533ikeDHvJd42nouRRx5wBzt36JuviiGe5dIPyHON/d0/Up+PBo6XkQ=="
},
"is-ssh": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.1.tgz",
@ -18771,6 +18776,11 @@
}
}
},
"memoize-one": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.0.4.tgz",
"integrity": "sha512-P0z5IeAH6qHHGkJIXWw0xC2HNEgkx/9uWWBQw64FJj3/ol14VYdfVGWWr0fXfjhhv3TKVIqUq65os6O4GUNksA=="
},
"memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",

View file

@ -29,8 +29,10 @@
"@uppy/utils": "1.1.0",
"classnames": "^2.2.6",
"cuid": "^2.1.1",
"is-shallow-equal": "^1.0.1",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"memoize-one": "^5.0.4",
"preact": "8.2.9",
"preact-css-transition-group": "^1.3.0",
"prettier-bytes": "^1.0.4",

View file

@ -1,12 +1,12 @@
const { h } = require('preact')
const classNames = require('classnames')
const pure = require('../../utils/pure')
const FilePreviewAndLink = require('./FilePreviewAndLink')
const FileProgress = require('./FileProgress')
const FileInfo = require('./FileInfo')
const Buttons = require('./Buttons')
module.exports = function FileItem (props) {
module.exports = pure(function FileItem (props) {
const file = props.file
const isProcessing = file.progress.preprocess || file.progress.postprocess
@ -72,4 +72,4 @@ module.exports = function FileItem (props) {
</div>
</li>
)
}
})

View file

@ -9,6 +9,32 @@ module.exports = (props) => {
{ 'uppy-Dashboard-files--noFiles': noFiles }
)
const fileProps = {
// FIXME This is confusing, it's actually the Dashboard's plugin ID
id: props.id,
error: props.error,
// TODO move this to context
i18n: props.i18n,
log: props.log,
info: props.info,
// features
acquirers: props.acquirers,
resumableUploads: props.resumableUploads,
individualCancellation: props.individualCancellation,
// visual options
hideRetryButton: props.hideRetryButton,
hidePauseResumeCancelButtons: props.hidePauseResumeCancelButtons,
showLinkToFileUploadResult: props.showLinkToFileUploadResult,
isWide: props.isWide,
metaFields: props.metaFields,
// callbacks
retryUpload: props.retryUpload,
pauseUpload: props.pauseUpload,
cancelUpload: props.cancelUpload,
toggleFileCard: props.toggleFileCard,
removeFile: props.removeFile
}
return (
<ul
class={dashboardFilesClass}
@ -16,7 +42,7 @@ module.exports = (props) => {
tabindex="-1">
{Object.keys(props.files).map((fileID) => (
<FileItem
{...props}
{...fileProps}
file={props.files[fileID]}
/>
))}

View file

@ -12,6 +12,7 @@ const cuid = require('cuid')
const ResizeObserver = require('resize-observer-polyfill').default || require('resize-observer-polyfill')
const { defaultPickerIcon } = require('./components/icons')
const createSuperFocus = require('./utils/createSuperFocus')
const memoize = require('memoize-one').default || require('memoize-one')
const TAB_KEY = 9
const ESC_KEY = 27
@ -659,6 +660,52 @@ module.exports = class Dashboard extends Plugin {
this.superFocusOnEachUpdate()
}
startUpload = (ev) => {
this.uppy.upload().catch((err) => {
// Log error.
this.uppy.log(err.stack || err.message || err)
})
}
cancelUpload = (fileID) => {
this.uppy.removeFile(fileID)
}
saveFileCard = (meta, fileID) => {
this.uppy.setFileMeta(fileID, meta)
this.toggleFileCard()
}
_attachRenderFunctionToTarget = (target) => {
const plugin = this.uppy.getPlugin(target.id)
return {
...target,
icon: plugin.icon || this.opts.defaultPickerIcon,
render: plugin.render
}
}
_isTargetSupported = (target) => {
const plugin = this.uppy.getPlugin(target.id)
// If the plugin does not provide a `supported` check, assume the plugin works everywhere.
if (typeof plugin.isSupported !== 'function') {
return true
}
return plugin.isSupported()
}
_getAcquirers = memoize((targets) => {
return targets
.filter(target => target.type === 'acquirer' && this._isTargetSupported(target))
.map(this._attachRenderFunctionToTarget)
})
_getProgressIndicators = memoize((targets) => {
return targets
.filter(target => target.type === 'progressindicator')
.map(this._attachRenderFunctionToTarget)
})
render (state) {
const pluginState = this.getPluginState()
const { files, capabilities, allowNewUpload } = state
@ -710,46 +757,8 @@ module.exports = class Dashboard extends Plugin {
const isAllPaused = inProgressFiles.length !== 0 &&
pausedFiles.length === inProgressFiles.length
const attachRenderFunctionToTarget = (target) => {
const plugin = this.uppy.getPlugin(target.id)
return Object.assign({}, target, {
icon: plugin.icon || this.opts.defaultPickerIcon,
render: plugin.render
})
}
const isSupported = (target) => {
const plugin = this.uppy.getPlugin(target.id)
// If the plugin does not provide a `supported` check, assume the plugin works everywhere.
if (typeof plugin.isSupported !== 'function') {
return true
}
return plugin.isSupported()
}
const acquirers = pluginState.targets
.filter(target => target.type === 'acquirer' && isSupported(target))
.map(attachRenderFunctionToTarget)
const progressindicators = pluginState.targets
.filter(target => target.type === 'progressindicator')
.map(attachRenderFunctionToTarget)
const startUpload = (ev) => {
this.uppy.upload().catch((err) => {
// Log error.
this.uppy.log(err.stack || err.message || err)
})
}
const cancelUpload = (fileID) => {
this.uppy.removeFile(fileID)
}
const saveFileCard = (meta, fileID) => {
this.uppy.setFileMeta(fileID, meta)
this.toggleFileCard()
}
const acquirers = this._getAcquirers(pluginState.targets)
const progressindicators = this._getProgressIndicators(pluginState.targets)
return DashboardUI({
state,
@ -794,16 +803,16 @@ module.exports = class Dashboard extends Plugin {
metaFields: pluginState.metaFields,
resumableUploads: capabilities.resumableUploads || false,
individualCancellation: capabilities.individualCancellation,
startUpload,
startUpload: this.startUpload,
pauseUpload: this.uppy.pauseResume,
retryUpload: this.uppy.retryUpload,
cancelUpload,
cancelUpload: this.cancelUpload,
cancelAll: this.uppy.cancelAll,
fileCardFor: pluginState.fileCardFor,
toggleFileCard: this.toggleFileCard,
toggleAddFilesPanel: this.toggleAddFilesPanel,
showAddFilesPanel: pluginState.showAddFilesPanel,
saveFileCard,
saveFileCard: this.saveFileCard,
width: this.opts.width,
height: this.opts.height,
showLinkToFileUploadResult: this.opts.showLinkToFileUploadResult,

View file

@ -0,0 +1,21 @@
const shallowEqual = require('is-shallow-equal')
const { h, Component } = require('preact')
/**
* Higher order component that doesn't rerender an element if its props didn't change.
*/
module.exports = function pure (Inner) {
return class Pure extends Component {
shouldComponentUpdate (nextProps) {
return !shallowEqual(this.props, nextProps)
}
render () {
// we have to clone this or Preact mutates it:
// https://github.com/preactjs/preact/issues/836
// TODO can be removed if we upgrade to Preact X
const props = { ...this.props }
return <Inner {...props} />
}
}
}