preact
preact copied to clipboard
Debug component thrashing
Proposed Error detail page on preactjs.com (hit edit on this PR for source):
---
url: /errors/1
title: Preact Error 1: Component Thrashing
---
Error 1: Component Thrashing
This problem occurs when components are redefined every time Preact renders.
This means that the component and all of its descendants will be destroyed and recreated on every render.
Background
Virtual DOM works by building up a new description of your application's structure every time something changes. To make this work with acceptable performance, Virtual DOM libraries implement a diffing algorithm that compares this new structure to the previous one (the structure of the previous render).
The first step of this process is to go through the tree of nodes created by your JSX - called VNodes - and match them up with the previous ones. In order to do this, it must enforce a definition of what makes two equivalent. This is done by comparing the VNode's type:
<meter value="50">home</a>
^-type ^-props ^-children
type === "meter"
<Avatar user="1" />
^-type ^-props
type === Avatar
Two VNodes are considered equal when their types are the same. When a VNode's type changes from one render to the next, any components and DOM that has been created for that VNode and all of its descendants must be destroyed in order to ensure output doesn't "bleed" across renders.
The Problem
Now think about how where we define components. When a component (class or function) is defined inside of a function, each time that outer function is called we'll get a new copy of that component:
function outer() {
const Avatar = ({ src }) => <img src={src} />
return Avatar
}
outer() !== outer() // a new Avatar function is created every time
This is the same as what the Virtual DOM library sees when looking at the JSX returned from your render functions or functional components. Take a look at what happens when we define Avatar from inside of another component's render:
function Profile(props) {
const Avatar = ({ src }) => <img src={src} />
return <Avatar src={props.src} />
}
const render1 = Profile({ src: '/1.gif' })
const render2 = Profile({ src: '/1.gif' })
render1.type !== render2.type
// They're not equal!
// This means we have to destroy and recreate the image.
This creation of new components on each render is how "Component Thrashing" comes to be.
How to Fix Component Thrashing
Let's take our Profile example from above:
function Profile(props) {
const Avatar = ({ src }) => <img src={src} /> // 🚨
return <Avatar src={props.src} />
}
We now know that Avatar is going to be created every time Profile is called by Preact.
To fix this, move the component's definition out of the outer function:
function Profile(props) {
return <Avatar src={props.src} />
}
const Avatar = ({ src }) => <img src={src} />
Now, every time Profile() is called, it will return the same reference to Avatar! It's generally best to write nested components this way so they can't rely on scoped variables, as this makes them "pure".
This will correct the issue and allow Preact to diff properly once again.
Rebased, ready for re-review + merge.
Coverage decreased (-0.2%) to 99.769% when pulling c0e710fa4b6646f0cb53708a5dc608be3431ce17 on debug-component-thrashing into c696534281a1d964a4023c24b0a4f21d60d539c5 on master.
Coverage decreased (-1.08%) to 98.67% when pulling 356ba58b58b01edd51ca5cdb0c9cd4b5e1ef06e8 on debug-component-thrashing into 97c0a2c7a97b79bb7d51593d61c813fc5d6bc3b5 on master.
took a stab at adding caching in WeakMap-supporting browsers.
Also fixed an issue where options._diff wasn't chained for null vnodes.