Skip to content

Commit 4deec23

Browse files
committed
feat(ObserveViewport): reduce cpu usage on idle
The CPU usage on low level devices was in some situations up to 50% all the time because every ObserveViewport component on the page did its own calculations whether to update or not. The work performed by those components was always the same. To reduce the stress on the CPU the Provider component now does all the required work and notifies its children as soon as something changes. To make this performant it was required to wrap the setState function of the ObserveViewport components, which then rerenders children if required, into another requestAnimationFrame which is considered an breaking change (even if I was not able to see any differences within the examples). The onUpdate property is still fired on the first update and should anyhow be used in performance heavy situations.
1 parent 5c04fd8 commit 4deec23

File tree

4 files changed

+254
-138
lines changed

4 files changed

+254
-138
lines changed

lib/ObserveViewport.tsx

Lines changed: 63 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,46 @@
11
import * as React from 'react';
2-
const memoizeOne = require('memoize-one');
3-
const memoize =
4-
typeof memoizeOne === 'function' ? memoizeOne : memoizeOne.default;
52
const raf = require('raf');
63

7-
import { shallowEqualScroll, shallowEqualDimensions } from './utils';
8-
import { IScroll, IDimensions } from './types';
9-
104
import {
115
Consumer,
12-
createInitScrollState,
136
createInitDimensionsState,
14-
IScroll as IContextScroll,
15-
SCROLL_DIR_UP,
16-
SCROLL_DIR_DOWN,
17-
SCROLL_DIR_RIGHT,
18-
SCROLL_DIR_LEFT,
7+
createInitScrollState,
198
} from './ViewportProvider';
20-
21-
interface IState extends IContextScroll, IDimensions {}
9+
import {
10+
IScroll,
11+
IDimensions,
12+
IViewport,
13+
TViewportChangeHandler,
14+
} from './types';
2215

2316
export interface IChildProps {
2417
scroll: IScroll | null;
2518
dimensions: IDimensions | null;
2619
}
2720

21+
interface IState extends IChildProps {}
22+
2823
interface IProps {
2924
children?: (props: IChildProps) => React.ReactNode;
3025
onUpdate?: (props: IChildProps) => void;
3126
disableScrollUpdates: boolean;
3227
disableDimensionsUpdates: boolean;
3328
}
3429

30+
interface IContext {
31+
addViewportChangeListener: (fn: TViewportChangeHandler) => void;
32+
removeViewportChangeListener: (fn: TViewportChangeHandler) => void;
33+
}
34+
3535
export default class ObserveViewport extends React.Component<IProps, IState> {
36-
tickId: NodeJS.Timer;
37-
scrollContext: IContextScroll;
38-
dimensionsContext: IDimensions;
36+
private addViewportChangeListener:
37+
| ((fn: TViewportChangeHandler) => void)
38+
| null;
39+
private removeViewportChangeListener:
40+
| ((fn: TViewportChangeHandler) => void)
41+
| null;
42+
43+
private tickId: NodeJS.Timer;
3944

4045
static defaultProps = {
4146
disableScrollUpdates: false,
@@ -44,100 +49,70 @@ export default class ObserveViewport extends React.Component<IProps, IState> {
4449

4550
constructor(props: IProps) {
4651
super(props);
47-
this.scrollContext = createInitScrollState();
48-
this.dimensionsContext = createInitDimensionsState();
4952
this.state = {
50-
...this.scrollContext,
51-
...this.dimensionsContext,
53+
scroll: createInitScrollState(),
54+
dimensions: createInitDimensionsState(),
5255
};
5356
}
5457

5558
shouldComponentUpdate(nextProps: IProps) {
5659
return Boolean(nextProps.children);
5760
}
5861

59-
componentDidMount() {
60-
this.tick(this.syncState);
61-
}
62-
6362
componentWillUnmount() {
63+
if (this.removeViewportChangeListener) {
64+
this.removeViewportChangeListener(this.handleViewportUpdate);
65+
}
66+
this.removeViewportChangeListener = null;
67+
this.addViewportChangeListener = null;
6468
raf.cancel(this.tickId);
6569
}
6670

67-
getPublicScroll = memoize(
68-
(scroll: IScroll): IScroll => scroll,
69-
shallowEqualScroll,
70-
);
71-
72-
getPublicDimensions = memoize(
73-
(dimensions: IDimensions): IDimensions => dimensions,
74-
shallowEqualDimensions,
75-
);
76-
77-
storeContext = (scrollContext: {
78-
scroll: IContextScroll;
79-
dimensions: IDimensions;
80-
}) => {
81-
this.scrollContext = scrollContext.scroll;
82-
this.dimensionsContext = scrollContext.dimensions;
83-
return null;
84-
};
71+
handleViewportUpdate = (viewport: IViewport) => {
72+
const scroll = this.props.disableScrollUpdates ? null : viewport.scroll;
73+
const dimensions = this.props.disableDimensionsUpdates
74+
? null
75+
: viewport.dimensions;
76+
const nextViewport = {
77+
scroll: scroll,
78+
dimensions: dimensions,
79+
};
80+
81+
if (this.props.onUpdate) {
82+
this.props.onUpdate(nextViewport);
83+
}
8584

86-
tick(updater: () => void) {
8785
this.tickId = raf(() => {
88-
if (this) {
89-
updater();
90-
this.tick(updater);
91-
}
86+
this.setState(nextViewport);
9287
});
93-
}
88+
};
9489

95-
syncState = () => {
96-
const { disableScrollUpdates, disableDimensionsUpdates } = this.props;
97-
const nextState = {
98-
...this.scrollContext,
99-
...this.dimensionsContext,
100-
};
101-
const scrollDidUpdate = disableScrollUpdates
102-
? false
103-
: !shallowEqualScroll(nextState as any, this.state as any);
104-
const dimensionsDidUpdate = disableDimensionsUpdates
105-
? false
106-
: !shallowEqualDimensions(nextState as any, this.state as any);
107-
108-
if (scrollDidUpdate || dimensionsDidUpdate) {
109-
if (this.props.onUpdate) {
110-
this.props.onUpdate(this.getPropsFromState(nextState));
111-
}
112-
this.setState(nextState);
90+
registerViewportListeners = ({
91+
addViewportChangeListener,
92+
removeViewportChangeListener,
93+
}: IContext): null => {
94+
const shouldRegister =
95+
this.removeViewportChangeListener !== removeViewportChangeListener &&
96+
this.addViewportChangeListener !== addViewportChangeListener;
97+
98+
if (!shouldRegister) {
99+
return null;
113100
}
114-
};
115101

116-
getPropsFromState(state: IState = this.state) {
117-
const { disableScrollUpdates, disableDimensionsUpdates } = this.props;
118-
const { xDir, yDir, width, height, ...scroll } = state;
119-
return {
120-
scroll: disableScrollUpdates
121-
? null
122-
: this.getPublicScroll({
123-
...scroll,
124-
isScrollingUp: yDir === SCROLL_DIR_UP,
125-
isScrollingDown: yDir === SCROLL_DIR_DOWN,
126-
isScrollingLeft: xDir === SCROLL_DIR_LEFT,
127-
isScrollingRight: xDir === SCROLL_DIR_RIGHT,
128-
}),
129-
dimensions: disableDimensionsUpdates
130-
? null
131-
: this.getPublicDimensions({ width, height }),
132-
};
133-
}
102+
if (this.removeViewportChangeListener) {
103+
this.removeViewportChangeListener(this.handleViewportUpdate);
104+
}
105+
this.removeViewportChangeListener = removeViewportChangeListener;
106+
addViewportChangeListener(this.handleViewportUpdate);
107+
return null;
108+
};
134109

135110
render() {
136111
const { children } = this.props;
137112
return (
138113
<React.Fragment>
139-
<Consumer>{this.storeContext}</Consumer>
140-
{typeof children === 'function' && children(this.getPropsFromState())}
114+
<Consumer>{this.registerViewportListeners}</Consumer>
115+
{typeof children === 'function' && children(this.state)}
141116
</React.Fragment>
142117
);
143118
}

0 commit comments

Comments
 (0)