First, read this and everything it references below.
- Force layout/reflow only when it's needed and not every frame. The browser has to recalculate all the page layout.
// BAD
function update() {
// trigger layout/reflow every frame
const rect = node.getBoundingClientRect();
//process
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// GOOD
let rect;
function resize() {
// trigger layout/reflow on window resize and cache the value
rect = node.getBoundingClientRect();
}
resize();
window.addEventListener("resize", resize, false);
function update() {
//process
requestAnimationFrame(update);
}
requestAnimationFrame(update);
- Reduce the amount of DOM elements, avoid using unnecessary elements. The more elements you have the more time the browser will take to recalculate. Also, try to keep the HTML as semantic as possible
// BAD
<div class="accordion">
<div class="accordion__head">
<div>
<span>title</span>
<div>
<div>
<div class="accordion__content">
<div>
<span>content</span>
<div>
<div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
// GOOD
<div class="accordion">
<div class="accordion__head">
<p>title</p>
<div>
<div class="accordion__content">
...content
<div></div>
</div>
</div>
</div>
</div>
- Do not update React state too often, only render it when it's needed. Every time a state changes it will render the component and all its children. You also can use
useMemo
to avoid rendering. Useconsole.log('update')
above the return to see how many times the component is rendered.
// BAD
const Cursor = () => {
const [mouse,setMouse] = useState({x=0,y=0})
function onMouseMove({pageX,pageY}) {
// render component every mousemove events
setMouse({x:pageX,y:pageY})
}
useLayoutEffect(()=>{
window.addEventListener('mousemove',onMouseMove,false)
},[])
console.log('update')
return <div style={"transform":`translate3d(${x}px,${y}px,0)`}></div>
}
// GOOD
const Cursor = () => {
const ref = useRef();
function onMouseMove({ pageX, pageY }) {
// render component once
ref.current.style.setProperty(
"transform",
`translate3d(${pageX}px,${pageY}px,0)`
);
}
useLayoutEffect(() => {
window.addEventListener("mousemove", onMouseMove, false);
}, []);
console.log("update");
return <div ref={ref}></div>;
};
- Prefer using CSS transition over Spring when it's possible. Animation libs such as GSAP or react-spring apply styles on every frame on the targeted elements. It triggers layout/reflow every frame versus toggling a class/style only triggers layout/reflow once.
// BAD
const Fade = ({ children, active }) => {
const [style] = useSpring(
() => ({
opacity: active ? 1 : 0,
}),
[active]
);
return <a.div style={style}>{children}</a.div>;
};
// GOOD
const Fade = ({children, active}) => {
return (
<div style={"opacity": active ? 1 : 0, "transition": "opacity 1s ease-out"}>
{children}
</div>
)
}
- Only use a single, unique, requestAnimationFrame. See performance benchmark here.
// BAD
function update() {
// process
requestAnimationFrame(update);
}
requestAnimationFrame(update);
import { raf } from "@react-spring/rafz";
// GOOD
function update() {
// process
return true;
}
raf.onFrame(update);
- Leverage hardware acceleration on costly CSS operations. Add
will-change: transform
ortransform: translateZ(0)
to force hardware acceleration.
// BAD
function onMouseMove({ pageX, pageY }) {
// no hardware acceleration
node.style.setProperty("transform", `translate(${pageX}px,${pageY}px)`);
}
window.addEventListener("mousemove", onMouseMove, false);
// GOOD
function onMouseMove({ pageX, pageY }) {
// translate3d leverage hardware acceleration
node.style.setProperty("transform", `translate3d(${pageX}px,${pageY}px,0)`);
}
window.addEventListener("mousemove", onMouseMove, false);
- Prefer animation optimized CSS properties as
opacity
ortransform
. See here.
// BAD
function onMouseMove({ pageX, pageY }) {
// animating top is costly
node.style.setProperty("left", `${pageX}px`);
node.style.setProperty("top", `${pageY}px`);
}
window.addEventListener("mousemove", onMouseMove, false);
// GOOD
function onMouseMove({ pageX, pageY }) {
// animating transform is cheap
node.style.setProperty("transform", `translate3d(${pageX}px,${pageY}px,0)`);
}
window.addEventListener("mousemove", onMouseMove, false);
- Use your browser devtools performances tab to figure out costly operations. Keep in mind to run 60fps, a browser frame execution duration shouldn't exceed 16ms.
- Reduce CSS size. Using media queries, prefer adding CSS properties than remove. The less CSS properties has an element the faster it will be to recalculate on layout/reflow. It's better for bundle size too.
// BAD
nav {
transform: translateY(100%);
opacity: 0;
&--open {
transform: translateY(0);
oapcity: 1;
}
}
// GOOD
nav {
// removed 2 lines
&--close {
transform: translateY(100%);
opacity: 0;
}
}
- Consider animate only elements visible on screen. Use
IntersectionObserver
. Withlocomotive-scroll
it's always better to usedata-scroll-section
. It will transform only visible parts of your page instead of all page.
// BAD
<div data-scroll-container>
<section></section>
<section></section>
<footer></footer>
</div>
// GOOD
<div data-scroll-container>
<section data-scroll-section></section>
<section data-scroll-section></section>
<footer data-scroll-section></footer>
</div>
- Use
next/image
. Don't forget to usesizes
prop:(min-width: 800px) [desktop size]vw, [mobile]vw
. Applypriority
prop on first visible images on landing as hero. Images below can just takeloading="eager"
// BAD <image src="image.jpg"></image>
// GOOD <image src="image.jpg" sizes="(min-width: 800px) 30vw, 100vw"></image>
- Only add a component to its specific use case. In other words, prefer to remove the component than to disable it. So the component's effect will be executed only when it's necessary.
// BAD
<Slider enabled="{mobile}">
<div></div>
<div></div>
<div></div>
<Slider></Slider
></Slider>
// GOOD { mobile ?
<Slider>
<div></div>
<div></div>
<div></div>
<Slider>
:
<div>
<div></div>
<div></div>
<div></div>
<div>}</div>
</div></Slider
></Slider
>
Every class should have only one responsibility SOLID. Its name should describe the exact behaviour without any doubts.
// BAD
.hidden-overflow {
@include mobile {
overflow: hidden;
}
}
// GOOD
.hidden-overflow {
overflow: hidden;
}
or .hidden-overflow-mobile {
@include mobile {
overflow: hidden;
}
}