Writing a Slow
React App 🐌
ABOUT ME 👨🏻💻
Sudhanshu Yadav
- Front-end Architect at HackerRank.
- Author of Brahmos, react-number-format,
packagebind and other OSS projects. - Organizes The Internals.
AGENDA 📋
What and Why?
- Patterns leading to a performance bottleneck.
- Why these patterns are slow in React?
- How to fix them?
- Will talk about React Internals & Micro-optimization.
- Will see lots of code today.
Expensive Renders
// bigList can have 1000's of items
function BigRender({ bigList }) {
return (
<div>
<h3>Big List</h3>
<ul className="items">
{bigList.map(item => (
<li className="item" key={item.id}>
<h4 className="item-title">{item.title}</h4>
<p>{item.description}</p>
{/*... */}
</li>
))}
</ul>
</div>
);
}
Big Render methods ❌
// bigList can have 1000's of items
function BigRender({ bigList }) {
return (
<div>
<h3>Big List</h3>
<ul className="items">
<Virtualize items={bigList}>
{item => (
<li className="item" key={item.id}>
<h4 className="item-title">{item.title}</h4>
<p>{item.description}</p>
{/*... */}
</li>
)}
</Virtualize>
</ul>
</div>
);
}
VirTUALIZE ✅
How React RENDERING WORKS?
function Greet({ name }) {
return (
<div className="greeting">
Hello <FancyName name={name} /> !!
</div>
);
}
You write in JSX.
function Greet({ name }) {
return React.createElement(
"div", //element type
{ className: "greeting" }, //props
// followed by children
"Hello ",
React.createElement(FancyName, { name: name }),
" !!"
);
}
JSX is transformed to createElement
{
type: "div",
props: {
className: "greeting",
children: [
"Hello ",
{
type: FancyName,
props: { name: name }
},
" !!"
]
}
};
createElement returns a data structure (VDOM)
// fiber structure
{
type: NodeConstructor, // div, FancyName
stateNode: NodeInstance, // HTMLDivElement, FancyNameInstance
child: childFiber,
sibling: siblingFiber,
return: parentFiber,
}
reactElements are mapped to a Fiber node.
{
type: "div", // <- parent
props: {
className: "greeting",
children: [
"Hello ", // <- Child text head
{
// Child FancyName (sibling of text head)
type: FancyName,
props: { name: name }
},
" !!" // <- child text tail (sibling of FancyName)
]
}
}
React process each fiber node one at a time and can pause in middle.
// bigList can have 1000's of items
function BigRender({ bigList }) {
return (
<div>
<h3>Big List</h3>
<ul className="items">
{bigList.map(item => (
<li className="item" key={item.id}>
<h4 className="item-title">{item.title}</h4>
<p>{item.description}</p>
{/*... */}
</li>
))}
</ul>
</div>
);
}
Big Render methods ❌
function ListItem({ item }) {
return (
<li className="item">
<h4 className="item-title">{item.title}</h4>
<p>{item.description}</p>
{/*... */}
</li>
);
}
function BigRender({ bigList }) {
return (
<div>
<h3>Big List</h3>
<ul className="items">
{bigList.map(item => (
<ListItem key={item.id} item={item} />
))}
</ul>
</div>
);
}
BReAK THE COMPONENT ✅
function ExpensiveRender({ data }) {
const value = someExpensiveCalculation(data);
return (
<div>
<h3>Expensive Calculation</h3>
<div className="expansive-value">{value}</div>
</div>
);
}
Expensive Render ❌
function ExpensiveRender({ data }) {
const memoizedValue = useMemo(() => someExpensiveCalculation(data), [data]);
return (
<div>
<h3>Expensive Calculation</h3>
<div className="expansive-value">{memoizedValue}</div>
</div>
);
}
Memoize COMPUTATION ✅
CANCELLING THE DIFFING
function TestComponent({ factor, initialValue }) {
const [incrementedValue, increment] = useState(initialValue);
const onClick = () => {
increment(incrementedValue + factor);
};
const IncrementResult = () => (
<FancyResult>
<FancyResult.label>Incremented with {factor}</FancyResult.label>
<FancyResult.value>{incrementedValue}</FancyResult.value>
</FancyResult>
);
return (
<>
<button className="action" onClick={onClick}>
Action
</button>
<IncrementResult />
</>
);
}
INLINE COMPONENTS ❌
The Diffing Algorithm
- On rerender compare the previous tree with the new tree.
- For each element
- If a key is provided compare the element with the previous element with the same key.
- If the key is not provided compare the element with the previous element at the same Index.
- If a key is provided but there is no previous element with the same key compare it with null.
The Diffing Algorithm
Compare:
- If the type of the element is the same as the previous element's type, update the component.
- If the type is different remove the previous element and add the new one.
function TestComponent({ factor, initialValue }) {
const [incrementedValue, increment] = useState(initialValue);
const onClick = () => {
increment(incrementedValue + factor);
};
const IncrementResult = () => (
<FancyResult>
<FancyResult.label>Incremented with {factor}</FancyResult.label>
<FancyResult.value>{incrementedValue}</FancyResult.value>
</FancyResult>
);
return (
<>
<button className="action" onClick={onClick}>
Action
</button>
<IncrementResult />
</>
);
}
INLINE COMPONENTS ❌
const IncrementResult = ({factor, value}) => (
<FancyResult>
<FancyResult.label>Incremented with {factor}</FancyResult.label>
<FancyResult.value>{value}</FancyResult.value>
</FancyResult>
);
function TestComponent({ factor, initialValue }) {
const [incrementedValue, increment] = useState(initialValue);
const onClick = () => {
increment(incrementedValue + factor);
};
return (
<>
<button className="action" onClick={onClick}>
Action
</button>
<IncrementResult factor={factor} value={value}/>
</>
);
}
STABLE COMPONENT REFERENCE ✅
function withProps(Component, fixedProps) {
return props => {
<Component {...fixedProps} {...props} />;
};
}
function TestComponent({ factor, initialValue }) {
const [incrementedValue, increment] = useState(initialValue);
const IncrementResultWithProps = withProps(IncrementResult, { factor });
return (
<>
<button className="action" onClick={onClick}>
Action
</button>
<IncrementResultWithProps incrementedValue={incrementedValue} />
</>
);
}
CREATING HOC INSIDE COMPONENT ❌
function WrongKeyImplementation({ person }) {
const { name } = person;
return (
<div className="person">
<div className="name">{person.name}</div>
<PersonDescription key={person.name} person={person} />
</div>
);
}
// 1st render
<WrongKeyImplementation person={{name: 'Sudhanshu', ...otherInfo}} />
// 2nd render
<WrongKeyImplementation person={{name: 'Yadav', ...otherInfo}} />
INCORRECT USE OF KEY ❌
function WrongKeyImplementation({ person }) {
const { name } = person;
return (
<div className="person">
<div className="name">{person.name}</div>
<PersonDescription person={person} />
</div>
);
}
// 1st render
<WrongKeyImplementation person={{name: 'Sudhanshu', ...otherInfo}} />
// 2nd render
<WrongKeyImplementation person={{name: 'Yadav', ...otherInfo}} />
USE KEY ONLY WHEN REQUIRED ✅
function Items({ items }) {
const [orderedItems, setItems] = useState(items);
return (
<div>
<button onClick={() => setItems(shuffle(orderedItems))}>Shuffle</button>
{orderedItems.map((item, idx) => (
<Item key={idx} item={item} />
))}
</div>
);
}
USING INDEX AS KEY ❌
function Items({ items }) {
const [orderedItems, setItems] = useState(items);
return (
<div>
<button onClick={() => setItems(shuffle(orderedItems))}>Shuffle</button>
{orderedItems.map((item) => (
<Item key={item.id} item={item} />
))}
</div>
);
}
USE DETERMINISTIC KEY ✅
COST OF HIERARCHY
function PlainComponent({ theme, utils, i18n }) {
return <div>{/** ...content... */}</div>;
}
export default withTheme(withUtils(withI18n(PlainComponent)));
HighER ORDER COMPONENT ❌
function PlainComponent() {
return (
<Theme>
{theme => (
<Utils>
{utils => (
<I18N>
{i18n => (
<div>{/** ...content... */}</div>
)}
</I18N>
)}
</Utils>
)}
</Theme>
);
}
Render PROPS ❌
function PlainComponent(props) {
const theme = useTheme();
const utils = useUtils();
const i18n = useI18n(props);
return <div>{/** ...content... */}</div>;
}
Hooks ✅
function Title(props) {
return <h1 {...props} />;
}
function Para(props) {
return <p {...props} />;
}
function Block(props) {
return <div {...props} />;
}
function SimpleComponent() {
return (
<Block>
<Title>Some Title</Title>
<Para>Lorem ipsum ......</Para>
</Block>
);
}
OvER ABSTRACTION ❌
function SimpleComponent() {
return (
<div>
<h1>Some Title</h1>
<p>Lorem ipsum ......</p>
</div>
);
}
MEANINGFUL ABSTRACTION ✅
UNOPTIMIZED PROVIDERS & SELECTORS
function Founder({ organization }) {
const { name: organizationName, team } = organization;
const { name, bio } = team.founder;
return (
<div>
<div className="organization-name">{organizationName}</div>
<div className="founder-name">{name}</div>
<p className="founder-bio">{bio}</p>
</div>
);
}
connect(state => {
return {
organization: state.organization
};
})(Founder);
CoNNECTING TOO MUCH DATA ❌
0
Advanced issue found▲
function Founder({ organizationName, name, bio }) {
return (
<div>
<div className="organization-name">{organizationName}</div>
<div className="founder-name">{name}</div>
<p className="founder-bio">{bio}</p>
</div>
);
}
connect(state => {
const { name: organizationName, team } = state.organization;
const { name, bio } = team.founder;
return {
organizationName,
name,
bio
};
})(Founder);
CoNNECT REQUIRED DATA ✅
0
Advanced issue found▲
function Founder() {
const { organizationName, name, bio } = useSelector(state => {
const { name: organizationName, team } = state.organization;
const { name, bio } = team.founder;
return {
organizationName,
name,
bio
};
}, shallowEqual);
return (
<div>
<div className="organization-name">{organizationName}</div>
<div className="founder-name">{name}</div>
<p className="founder-bio">{bio}</p>
</div>
);
}
WITH USE SELECTOR ✅
0
Advanced issue found▲
function BadComponent({ storeState }) {
return (
<div>
{/** ...content... */}
</div>
)
}
connect(state => {
return {
storeState: state
};
});
CONNECTING WHOLE STATE ❌
0
Advanced issue found▲
function Jobs() {
const jobs = useSelector(state => {
const { job, jobIds } = state.jobs;
return jobIds.map(jobId => job[jobId]);
});
return (
<div>
{jobs.map(job => (
<Job jobData={job} key={job.id} />
))}
</div>
);
}
CREATING NEW REFERENCE ❌
0
Advanced issue found▲
function Jobs() {
const { job, jobIds } = useSelector(state => {
const { job, jobIds } = state.jobs;
return { job, jobIds };
}, shallowEqual);
const jobs = jobIds.map(jobId => job[jobId]);
return (
<div>
{jobs.map(job => (
<Job jobData={job} key={job.id} />
))}
</div>
);
}
MOVE LOGIC OUTSIDE ✅
0
Advanced issue found▲
const jobsSelector = createSelector(
[
state => state.job,
state => state.jobIds
],
(job, jobIds) => jobIds.map(jobId => job[jobId])
);
function Jobs() {
const jobs = useSelector(jobsSelector);
return (
<div>
{jobs.map(job => (
<Job jobData={job} key={job.id} />
))}
</div>
);
}
Use RESELECT ✅
const JobsContext = createContext();
function jobsReducer(state, action) {
//...
}
function App({ children }) {
const [state, dispatch] = useReducer(jobsReducer);
return (
<JobsContext.Provider value={[state, dispatch]}>
{children}
</JobsContext.Provider>
);
}
function JobDescription({ jobId }) {
// With this JobDescription will rerender even if unrelated job changes
const [jobs] = useContext(JobsContext);
const job = jobs[jobId];
return <div>{job.name}</div>;
}
UNOPTIMIZED CONTEXT ❌
function JobDescription({ job }) {
return <div>{job.name}</div>;
}
// Here the withContext can memoize component and
// rerender JobDescription only if job prop changes
withContext(JobsContext, (context, props) => {
const [jobs] = context;
const { jobId } = props;
return {
job: jobs[jobId]
};
})(JobDescription);
CONNECT WITH HOC ✅
UNOPTIMIZED OPTIMIZATION
class ChildPureComponent extends React.PureComponent {
render() {
// ...
}
}
const MemoizedComponent = React.memo(() => {
//...
});
MEMOIZING COMPONENT
function Example() {
const [state, setState] = useState("");
return (
<div>
<MemoizedComponent>
{/* Reference of children (React Element) will always be different. */}
<span>Hello World</span>
</MemoizedComponent>
</div>
);
}
BREAKING MEMOIZATION ❌
function Example() {
const [state, setState] = useState("");
return (
<div>
{/* Reference of onChange will always be different. */}
<MemoizedComponent
value={state}
onChange={changes => {
setState(changes);
}}
/>
</div>
);
}
BREAKING MEMOIZATION ❌
function Example() {
const [number, setNumber] = useState(0);
const [factor, setFactor] = useState(0);
// Will result unnecessary shallow compare if number/factor always changes
const onSubmit = useCallback(() => {
setNumber(number + factor);
}, [number, factor]);
return (
<form>
<div className="value">{number}</div>
<input value={factor} onChange={e => setFactor(e.target.value)} />
<button type="submit" onClick={onSubmit}>
Submit
</button>
</form>
);
}
Wrong uSE oF USE CALLBACK ❌
function Example() {
const [number, setNumber] = useState(0);
const [factor, setFactor] = useState(0);
const onSubmit = () => {
setNumber(number + factor);
};
return (
<form>
<div className="value">{number}</div>
<input value={factor} onChange={e => setFactor(e.target.value)} />
<button type="submit" onClick={onSubmit}>
Submit
</button>
</form>
);
}
Don't OVERUSE USE CALLBACK ✅
function MemoExample({ factor, children }) {
const someExpensiveFactorial = useMemo(() => {
const childrenCount = React.Children.count(children);
return computeExpensiveValue(factor, childrenCount);
}, [factor, children]); /*children reference will always change*/
return (
<div>
<span>Children Factorial: {someExpensiveFactorial}</span>
{children}
</div>
);
}
Incorrect USE OF USE MEMO ❌
function MemoExample({ factor, children }) {
const childrenCount = React.Children.count(children);
const someExpensiveFactorial = useMemo(() => {
return computeExpensiveValue(factor, childrenCount);
}, [factor, childrenCount]);
return (
<div>
<span>Children Factorial: {someExpensiveFactorial}</span>
{children}
</div>
);
}
MEMOIZE ON STABLE INPUTS ✅
class UpdateCheck extends React.Component {
shouldComponentUpdate(nextProps) {
return deepCompare(this.props, nextProps);
}
render() {
return <div>{/* ...contents... */}</div>;
}
}
Expensive UPDATE CHECK ❌
const UpdateCheck = React.memo(() => {
return <div>{/* ...contents... */}</div>;
}, deepCompare);
Expensive UPDATE CHECK ❌
THANK YOU 🙏
Writing a Slow React App
By Sudhanshu Yadav
Writing a Slow React App
- 817