Spinners versus skeletons in the battle of hasting

Martin Nuc

Martin Nuc

Software engineer at Productboard




Agenda
- Performance issues in PB
- Theory of loading states
- Two issues we had
- How Design system helped
"Productboard is slow to load"
Huge JS bundle
10.8 MB

Slice up the Productboard

โ๏ธ
split app into chunks by routes
const loadMyComponent = () =>
import(
/* webpackChunkName: "awesome-chunk" */
'src/js/MyComponent'
);
const LazyMyComponent = React.lazy(loadMyComponent);
<Suspense fallback={<LoadingSpinner />}>
<LazyMyComponent />
</Suspense>
Chunks (~60)


10.8 MB down to 3.8 MB
Delay when navigating
๐
How to identify loading state in the app?
UX Theory
๐ Loading spinner

๐ฅ Progress bar

โ ๏ธ Loading skeleton


Don't do's

โ Multiple spinners





โ No response on click
โ Short spinner
โ Long spinner
Chunks (~60)


Three cases
๐ really fast parts
๐ reasonably fast
๐ข slow large pages
300ms
1500ms
Spinner
Skeleton
No indicator
Two issues
- the fallback is always rendered
- identify slow pages
1st issue
the fallback is always rendered
๐จ
=
spinner shows even when not necessary
๐คฌ Suspense fallback is always rendered

Suspense
Suspense
- prevents rendering it's children component until its data are ready
- experimental
- API for React.lazy + Suspense won't change
const Suspense = ({children, fallback}) => {
try {
return children;
} catch {
return fallback;
}
}
๐ค When is children ready?
const Suspense = ({children, fallback}) => {
try {
return children;
} catch(thrownPromise) {
thrownPromise.then(() => rerender());
return fallback;
}
}
Fallback always rendered
const Suspense = ({children, fallback}) => {
try {
return children;
} catch(thrownPromise) {
thrownPromise.then(() => rerender());
return fallback;
}
}
First render always throws
๐ก postpone rendering of fallback
"DelayedComponent"
Usage
<Suspense fallback={
<DelayedComponent delay={300}>
<LoadingSpinner />
</DelayedComponent>
}>
<ComponentFetchingData />
</Suspense>
export const DelayedComponent = ({children, delay}) => {
const [shouldDisplay, setShouldDisplay] = useState(false);
useEffect(() => {
const timeoutReference = setTimeout(() => {
setShouldDisplay(true);
}, delay);
return () => {
clearTimeout(timeoutReference);
};
}, [delay]);
if (shouldDisplay) {
return children;
} else {
return null;
}
}
โ fallback renders only when needed
2nd issue
Identify slow pages
โ ๏ธ
๐ we had no visibility into components rendering
Identify slow pages
๐ก track DelayedComponent lifecycle
DelayedComponent
nothing
Spinner
Suspended content
loading done
300ms
โฑ rendered
โฑ fallback displayed
โฑ component unmounted




โ identified slow pages (>1500ms)
๐ง crafted skeletons for them
๐งฉ Design system
- helped tremendously!
- speedup crafting skeletons
๐


๐ฅก Take aways
- measure your app performance
- pick the best solution based on โคด๏ธ
- deliver the best UX to the user

Q&A
Is it perfect?
Spinner
300ms
350ms
โ Fast spinner here!
Finished loading
It's a trade of
Spinner
300ms
350ms
Finished loading
600ms
250ms
no fast spinner or faster render?
Spinners vs Skeletons
By Martin Nuc
Spinners vs Skeletons
- 204