Improve render performance

This commit is contained in:
Thomas Nordquist
2019-07-02 15:10:46 +02:00
parent aa05c16651
commit b5aa22a6d8
4 changed files with 120 additions and 104 deletions

View File

@@ -1,5 +1,5 @@
import * as q from '../../../../../backend/src/Model'
import React from 'react'
import React, { useEffect, useState, useMemo } from 'react'
import TreeNode from '.'
import { SettingsState } from '../../../reducers/Settings'
import { sortedNodes } from '../../../sortedNodes'
@@ -16,59 +16,54 @@ export interface Props {
selectTopicAction: (treeNode: q.TreeNode<any>) => void
settings: SettingsState
actions: typeof treeActions
theme: Theme
}
interface State {
alreadyAdded: number
}
function useStagedRendering(treeNode: q.TreeNode<any>) {
const [alreadyAdded, setAlreadyAdded] = useState(10)
const edges = treeNode.edgeArray
class TreeNodeSubnodes extends React.Component<Props, State> {
private renderMoreAnimationFrame?: any
constructor(props: Props) {
super(props)
this.state = { alreadyAdded: 10 }
}
useEffect(() => {
let renderMoreAnimationFrame: any
private renderMore() {
this.renderMoreAnimationFrame = (window as any).requestIdleCallback(
if (alreadyAdded < edges.length) {
renderMoreAnimationFrame = (window as any).requestIdleCallback(
() => {
this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 })
setAlreadyAdded(alreadyAdded * 1.5)
},
{ timeout: 500 }
)
}
public componentWillUnmount() {
;(window as any).cancelIdleCallback(this.renderMoreAnimationFrame)
return function cleanup() {
;(window as any).cancelIdleCallback(renderMoreAnimationFrame)
}
}, [alreadyAdded, edges.length])
public render() {
const edges = this.props.treeNode.edgeArray
if (edges.length === 0) {
return null
}
return alreadyAdded
}
if (this.state.alreadyAdded < edges.length) {
this.renderMore()
}
function TreeNodeSubnodes(props: Props) {
const alreadyAdded = useStagedRendering(props.treeNode)
const nodes = sortedNodes(this.props.settings, this.props.treeNode).slice(0, this.state.alreadyAdded)
return useMemo(() => {
const nodes = sortedNodes(props.settings, props.treeNode).slice(0, alreadyAdded)
const listItems = nodes.map(node => {
return (
<TreeNode
key={`${node.hash()}-${this.props.filter}`}
key={`${node.hash()}-${props.filter}`}
treeNode={node}
className={this.props.classes.listItem}
className={props.classes.listItem}
lastUpdate={node.lastUpdate}
selectTopicAction={this.props.selectTopicAction}
settings={this.props.settings}
actions={this.props.actions}
selectTopicAction={props.selectTopicAction}
settings={props.settings}
actions={props.actions}
/>
)
})
return <span className={this.props.classes.list}>{listItems}</span>
}
return <span className={props.classes.list}>{listItems}</span>
}, [alreadyAdded, props.lastUpdate, props.theme])
}
const styles = (theme: Theme) => ({
@@ -79,4 +74,4 @@ const styles = (theme: Theme) => ({
},
})
export default withStyles(styles)(TreeNodeSubnodes)
export default withStyles(styles, { withTheme: true })(TreeNodeSubnodes)

View File

@@ -1,22 +1,30 @@
import React, { useEffect } from 'react'
export function useAnimationToIndicateTopicUpdate(
ref: React.MutableRefObject<HTMLDivElement | undefined>,
lastUpdate: number,
className: string,
selected: boolean,
setShowUpdateAnimation: React.Dispatch<React.SetStateAction<boolean>>,
showUpdateAnimation: boolean
shouldAnimate: boolean
) {
useEffect(() => {
if (Date.now() - lastUpdate < 3000 && !selected) {
setShowUpdateAnimation(true)
}
}, [lastUpdate, selected])
useEffect(() => {
if (showUpdateAnimation) {
const timeout = setTimeout(() => setShowUpdateAnimation(false), 500)
if (ref.current && shouldAnimate && Date.now() - lastUpdate < 3000 && !selected) {
let animationFrame = requestAnimationFrame(() => {
ref.current && ref.current.classList.add(className)
})
const timeout = setTimeout(
() =>
(animationFrame = requestAnimationFrame(() => {
ref.current && ref.current.classList.remove(className)
})),
500
)
return function cleanup() {
clearTimeout(timeout)
animationFrame && cancelAnimationFrame(animationFrame)
}
}
}, [showUpdateAnimation])
}, [lastUpdate, selected, shouldAnimate, className])
}

View File

@@ -1,56 +1,16 @@
import * as q from '../../../../../backend/src/Model'
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import TreeNodeSubnodes from './TreeNodeSubnodes'
import TreeNodeTitle from './TreeNodeTitle'
import { SettingsState } from '../../../reducers/Settings'
import { styles } from './styles'
import { Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions'
import { treeActions } from '../../../actions'
import { lightBlue, teal, amber, green, deepPurple, blueGrey } from '@material-ui/core/colors'
import { useAnimationToIndicateTopicUpdate } from './effects/useAnimationToIndicateTopicUpdate'
import { useDeleteKeyCallback } from './effects/useDeleteKeyCallback'
import { useIsAllowedToAutoExpandState } from './effects/useIsAllowedToAutoExpandState'
const styles = (theme: Theme) => {
return {
collapsedSubnodes: {
color: theme.palette.text.secondary,
},
displayBlock: {
display: 'block',
},
node: {
display: 'block',
marginLeft: '10px',
'&:hover': {
backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light,
},
},
topicSelect: {
float: 'right' as 'right',
opacity: 0,
cursor: 'pointer',
marginTop: '-1px',
},
subnodes: {
marginLeft: theme.spacing(1.5),
},
selected: {
backgroundColor: (theme.palette.type === 'light' ? blueGrey[300] : theme.palette.primary.main) + ' !important',
},
hover: {},
title: {
borderRadius: '4px',
lineHeight: '1em',
display: 'inline-block' as 'inline-block',
whiteSpace: 'nowrap' as 'nowrap',
padding: '1px 4px 0px 4px',
height: '14px',
margin: '1px 0px 2px 0px',
},
}
}
import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions'
export interface Props {
isRoot?: boolean
@@ -69,13 +29,21 @@ export interface Props {
function TreeNodeComponent(props: Props) {
const { actions, classes, className, settings, theme, treeNode, lastUpdate, name } = props
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
const [showUpdateAnimation, setShowUpdateAnimation] = useState(false)
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined)
const [selected, setSelected] = useState(false)
const nodeRef = useRef<HTMLDivElement>()
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride)
useAnimationToIndicateTopicUpdate(lastUpdate, selected, setShowUpdateAnimation, showUpdateAnimation)
const animationClass =
props.theme.palette.type === 'light' ? props.classes.animationLight : props.classes.animationDark
useAnimationToIndicateTopicUpdate(
nodeRef,
lastUpdate,
animationClass,
selected,
settings.get('highlightTopicUpdates')
)
const isCollapsed =
Boolean(collapsedOverride) === collapsedOverride ? Boolean(collapsedOverride) : !isAllowedToAutoExpand
@@ -141,19 +109,12 @@ function TreeNodeComponent(props: Props) {
)
}
const shouldStartAnimation = settings.get('highlightTopicUpdates') && showUpdateAnimation
const animationName = theme.palette.type === 'light' ? 'updateLight' : 'updateDark'
const animation = shouldStartAnimation
? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` }
: {}
const highlightClass = selected ? classes.selected : ''
return (
<div>
<div
key={treeNode.hash()}
className={`${classes.node} ${className} ${highlightClass} ${classes.title}`}
style={animation}
onMouseEnter={mouseOver}
onFocus={didObtainFocus}
onClick={didClickTitle}
@@ -172,7 +133,7 @@ function TreeNodeComponent(props: Props) {
{renderNodes()}
</div>
)
}, [lastUpdate, treeNode, name, isCollapsed, selected, theme, showUpdateAnimation])
}, [lastUpdate, treeNode, name, isCollapsed, selected, theme])
}
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)

View File

@@ -0,0 +1,52 @@
import { blueGrey } from '@material-ui/core/colors'
import { Theme } from '@material-ui/core/styles'
export const styles = (theme: Theme) => {
return {
animationLight: {
willChange: 'auto',
translateZ: 0,
animation: `updateLight 0.5s`,
},
animationDark: {
willChange: 'auto',
translateZ: 0,
animation: `updateLight 0.5s`,
},
collapsedSubnodes: {
color: theme.palette.text.secondary,
},
displayBlock: {
display: 'block',
},
node: {
display: 'block',
marginLeft: '10px',
'&:hover': {
backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light,
},
},
topicSelect: {
float: 'right' as 'right',
opacity: 0,
cursor: 'pointer',
marginTop: '-1px',
},
subnodes: {
marginLeft: theme.spacing(1.5),
},
selected: {
backgroundColor: (theme.palette.type === 'light' ? blueGrey[300] : theme.palette.primary.main) + ' !important',
},
hover: {},
title: {
borderRadius: '4px',
lineHeight: '1em',
display: 'inline-block' as 'inline-block',
whiteSpace: 'nowrap' as 'nowrap',
padding: '1px 4px 0px 4px',
height: '14px',
margin: '1px 0px 2px 0px',
},
}
}