]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/components/intersection_observer_article.js
Summary: fix slowness due to layout thrashing when reloading a large … (#12661)
[mastodon.git] / app / javascript / mastodon / components / intersection_observer_article.js
1 import React from 'react';
2 import PropTypes from 'prop-types';
3 import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
4 import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
5 import { is } from 'immutable';
6
7 // Diff these props in the "rendered" state
8 const updateOnPropsForRendered = ['id', 'index', 'listLength'];
9 // Diff these props in the "unrendered" state
10 const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
11
12 export default class IntersectionObserverArticle extends React.Component {
13
14 static propTypes = {
15 intersectionObserverWrapper: PropTypes.object.isRequired,
16 id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
17 index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
18 listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
19 saveHeightKey: PropTypes.string,
20 cachedHeight: PropTypes.number,
21 onHeightChange: PropTypes.func,
22 children: PropTypes.node,
23 currentlyViewing: PropTypes.number,
24 updateCurrentlyViewing: PropTypes.func,
25 };
26
27 state = {
28 isHidden: false, // set to true in requestIdleCallback to trigger un-render
29 }
30
31 shouldComponentUpdate (nextProps, nextState) {
32 const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
33 const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
34 if (!!isUnrendered !== !!willBeUnrendered) {
35 // If we're going from rendered to unrendered (or vice versa) then update
36 return true;
37 }
38 // Otherwise, diff based on props
39 const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
40 return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
41 }
42
43 componentDidMount () {
44 const { intersectionObserverWrapper, id } = this.props;
45
46 intersectionObserverWrapper.observe(
47 id,
48 this.node,
49 this.handleIntersection
50 );
51
52 this.componentMounted = true;
53
54 if(id === this.props.currentlyViewing) this.node.scrollIntoView();
55 }
56
57 componentWillUnmount () {
58 const { intersectionObserverWrapper, id } = this.props;
59 intersectionObserverWrapper.unobserve(id, this.node);
60
61 this.componentMounted = false;
62 }
63
64 handleIntersection = (entry) => {
65 this.entry = entry;
66
67 if(entry.intersectionRatio > 0.75 && this.props.updateCurrentlyViewing) this.props.updateCurrentlyViewing(this.id);
68
69 scheduleIdleTask(this.calculateHeight);
70 this.setState(this.updateStateAfterIntersection);
71 }
72
73 updateStateAfterIntersection = (prevState) => {
74 if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
75 scheduleIdleTask(this.hideIfNotIntersecting);
76 }
77 return {
78 isIntersecting: this.entry.isIntersecting,
79 isHidden: false,
80 };
81 }
82
83 calculateHeight = () => {
84 const { onHeightChange, saveHeightKey, id } = this.props;
85 // save the height of the fully-rendered element (this is expensive
86 // on Chrome, where we need to fall back to getBoundingClientRect)
87 this.height = getRectFromEntry(this.entry).height;
88
89 if (onHeightChange && saveHeightKey) {
90 onHeightChange(saveHeightKey, id, this.height);
91 }
92 }
93
94 hideIfNotIntersecting = () => {
95 if (!this.componentMounted) {
96 return;
97 }
98
99 // When the browser gets a chance, test if we're still not intersecting,
100 // and if so, set our isHidden to true to trigger an unrender. The point of
101 // this is to save DOM nodes and avoid using up too much memory.
102 // See: https://github.com/tootsuite/mastodon/issues/2900
103 this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
104 }
105
106 handleRef = (node) => {
107 this.node = node;
108 }
109
110 render () {
111 const { children, id, index, listLength, cachedHeight } = this.props;
112 const { isIntersecting, isHidden } = this.state;
113
114 if (!isIntersecting && (isHidden || cachedHeight)) {
115 return (
116 <article
117 ref={this.handleRef}
118 aria-posinset={index + 1}
119 aria-setsize={listLength}
120 style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
121 data-id={id}
122 tabIndex='0'
123 >
124 {children && React.cloneElement(children, { hidden: true })}
125 </article>
126 );
127 }
128
129 return (
130 <article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
131 {children && React.cloneElement(children, { hidden: false })}
132 </article>
133 );
134 }
135
136 }
This page took 0.102958 seconds and 5 git commands to generate.