Improve render performance
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
52
app/src/components/Tree/TreeNode/styles.ts
Normal file
52
app/src/components/Tree/TreeNode/styles.ts
Normal 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',
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user