Improve render performance
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import * as q from '../../../../../backend/src/Model'
|
import * as q from '../../../../../backend/src/Model'
|
||||||
import React from 'react'
|
import React, { useEffect, useState, useMemo } from 'react'
|
||||||
import TreeNode from '.'
|
import TreeNode from '.'
|
||||||
import { SettingsState } from '../../../reducers/Settings'
|
import { SettingsState } from '../../../reducers/Settings'
|
||||||
import { sortedNodes } from '../../../sortedNodes'
|
import { sortedNodes } from '../../../sortedNodes'
|
||||||
@@ -16,59 +16,54 @@ export interface Props {
|
|||||||
selectTopicAction: (treeNode: q.TreeNode<any>) => void
|
selectTopicAction: (treeNode: q.TreeNode<any>) => void
|
||||||
settings: SettingsState
|
settings: SettingsState
|
||||||
actions: typeof treeActions
|
actions: typeof treeActions
|
||||||
|
theme: Theme
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
function useStagedRendering(treeNode: q.TreeNode<any>) {
|
||||||
alreadyAdded: number
|
const [alreadyAdded, setAlreadyAdded] = useState(10)
|
||||||
|
const edges = treeNode.edgeArray
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let renderMoreAnimationFrame: any
|
||||||
|
|
||||||
|
if (alreadyAdded < edges.length) {
|
||||||
|
renderMoreAnimationFrame = (window as any).requestIdleCallback(
|
||||||
|
() => {
|
||||||
|
setAlreadyAdded(alreadyAdded * 1.5)
|
||||||
|
},
|
||||||
|
{ timeout: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return function cleanup() {
|
||||||
|
;(window as any).cancelIdleCallback(renderMoreAnimationFrame)
|
||||||
|
}
|
||||||
|
}, [alreadyAdded, edges.length])
|
||||||
|
|
||||||
|
return alreadyAdded
|
||||||
}
|
}
|
||||||
|
|
||||||
class TreeNodeSubnodes extends React.Component<Props, State> {
|
function TreeNodeSubnodes(props: Props) {
|
||||||
private renderMoreAnimationFrame?: any
|
const alreadyAdded = useStagedRendering(props.treeNode)
|
||||||
constructor(props: Props) {
|
|
||||||
super(props)
|
|
||||||
this.state = { alreadyAdded: 10 }
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderMore() {
|
return useMemo(() => {
|
||||||
this.renderMoreAnimationFrame = (window as any).requestIdleCallback(
|
const nodes = sortedNodes(props.settings, props.treeNode).slice(0, alreadyAdded)
|
||||||
() => {
|
|
||||||
this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 })
|
|
||||||
},
|
|
||||||
{ timeout: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
;(window as any).cancelIdleCallback(this.renderMoreAnimationFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const edges = this.props.treeNode.edgeArray
|
|
||||||
if (edges.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.alreadyAdded < edges.length) {
|
|
||||||
this.renderMore()
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = sortedNodes(this.props.settings, this.props.treeNode).slice(0, this.state.alreadyAdded)
|
|
||||||
const listItems = nodes.map(node => {
|
const listItems = nodes.map(node => {
|
||||||
return (
|
return (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
key={`${node.hash()}-${this.props.filter}`}
|
key={`${node.hash()}-${props.filter}`}
|
||||||
treeNode={node}
|
treeNode={node}
|
||||||
className={this.props.classes.listItem}
|
className={props.classes.listItem}
|
||||||
lastUpdate={node.lastUpdate}
|
lastUpdate={node.lastUpdate}
|
||||||
selectTopicAction={this.props.selectTopicAction}
|
selectTopicAction={props.selectTopicAction}
|
||||||
settings={this.props.settings}
|
settings={props.settings}
|
||||||
actions={this.props.actions}
|
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) => ({
|
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'
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
export function useAnimationToIndicateTopicUpdate(
|
export function useAnimationToIndicateTopicUpdate(
|
||||||
|
ref: React.MutableRefObject<HTMLDivElement | undefined>,
|
||||||
lastUpdate: number,
|
lastUpdate: number,
|
||||||
|
className: string,
|
||||||
selected: boolean,
|
selected: boolean,
|
||||||
setShowUpdateAnimation: React.Dispatch<React.SetStateAction<boolean>>,
|
shouldAnimate: boolean
|
||||||
showUpdateAnimation: boolean
|
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Date.now() - lastUpdate < 3000 && !selected) {
|
if (ref.current && shouldAnimate && Date.now() - lastUpdate < 3000 && !selected) {
|
||||||
setShowUpdateAnimation(true)
|
let animationFrame = requestAnimationFrame(() => {
|
||||||
}
|
ref.current && ref.current.classList.add(className)
|
||||||
}, [lastUpdate, selected])
|
})
|
||||||
useEffect(() => {
|
|
||||||
if (showUpdateAnimation) {
|
const timeout = setTimeout(
|
||||||
const timeout = setTimeout(() => setShowUpdateAnimation(false), 500)
|
() =>
|
||||||
|
(animationFrame = requestAnimationFrame(() => {
|
||||||
|
ref.current && ref.current.classList.remove(className)
|
||||||
|
})),
|
||||||
|
500
|
||||||
|
)
|
||||||
|
|
||||||
return function cleanup() {
|
return function cleanup() {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
|
animationFrame && cancelAnimationFrame(animationFrame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [showUpdateAnimation])
|
}, [lastUpdate, selected, shouldAnimate, className])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,16 @@
|
|||||||
import * as q from '../../../../../backend/src/Model'
|
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 TreeNodeSubnodes from './TreeNodeSubnodes'
|
||||||
import TreeNodeTitle from './TreeNodeTitle'
|
import TreeNodeTitle from './TreeNodeTitle'
|
||||||
import { SettingsState } from '../../../reducers/Settings'
|
import { SettingsState } from '../../../reducers/Settings'
|
||||||
|
import { styles } from './styles'
|
||||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||||
import { TopicViewModel } from '../../../model/TopicViewModel'
|
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||||
import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions'
|
|
||||||
import { treeActions } from '../../../actions'
|
import { treeActions } from '../../../actions'
|
||||||
import { lightBlue, teal, amber, green, deepPurple, blueGrey } from '@material-ui/core/colors'
|
|
||||||
import { useAnimationToIndicateTopicUpdate } from './effects/useAnimationToIndicateTopicUpdate'
|
import { useAnimationToIndicateTopicUpdate } from './effects/useAnimationToIndicateTopicUpdate'
|
||||||
import { useDeleteKeyCallback } from './effects/useDeleteKeyCallback'
|
import { useDeleteKeyCallback } from './effects/useDeleteKeyCallback'
|
||||||
import { useIsAllowedToAutoExpandState } from './effects/useIsAllowedToAutoExpandState'
|
import { useIsAllowedToAutoExpandState } from './effects/useIsAllowedToAutoExpandState'
|
||||||
|
import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions'
|
||||||
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',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
isRoot?: boolean
|
isRoot?: boolean
|
||||||
@@ -69,13 +29,21 @@ export interface Props {
|
|||||||
function TreeNodeComponent(props: Props) {
|
function TreeNodeComponent(props: Props) {
|
||||||
const { actions, classes, className, settings, theme, treeNode, lastUpdate, name } = props
|
const { actions, classes, className, settings, theme, treeNode, lastUpdate, name } = props
|
||||||
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
|
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
|
||||||
const [showUpdateAnimation, setShowUpdateAnimation] = useState(false)
|
|
||||||
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined)
|
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined)
|
||||||
const [selected, setSelected] = useState(false)
|
const [selected, setSelected] = useState(false)
|
||||||
const nodeRef = useRef<HTMLDivElement>()
|
const nodeRef = useRef<HTMLDivElement>()
|
||||||
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
|
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
|
||||||
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride)
|
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 =
|
const isCollapsed =
|
||||||
Boolean(collapsedOverride) === collapsedOverride ? Boolean(collapsedOverride) : !isAllowedToAutoExpand
|
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 : ''
|
const highlightClass = selected ? classes.selected : ''
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
key={treeNode.hash()}
|
key={treeNode.hash()}
|
||||||
className={`${classes.node} ${className} ${highlightClass} ${classes.title}`}
|
className={`${classes.node} ${className} ${highlightClass} ${classes.title}`}
|
||||||
style={animation}
|
|
||||||
onMouseEnter={mouseOver}
|
onMouseEnter={mouseOver}
|
||||||
onFocus={didObtainFocus}
|
onFocus={didObtainFocus}
|
||||||
onClick={didClickTitle}
|
onClick={didClickTitle}
|
||||||
@@ -172,7 +133,7 @@ function TreeNodeComponent(props: Props) {
|
|||||||
{renderNodes()}
|
{renderNodes()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [lastUpdate, treeNode, name, isCollapsed, selected, theme, showUpdateAnimation])
|
}, [lastUpdate, treeNode, name, isCollapsed, selected, theme])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
|
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