[go: up one dir, main page]

0% found this document useful (0 votes)
4 views12 pages

Web Optimisation

Uploaded by

m sahil hussian
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views12 pages

Web Optimisation

Uploaded by

m sahil hussian
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 12

Web Optimisation

What optimisation is: reduce time, CPU, memory and network work
required to get a usable site and keep it responsive.
Goal: improve user-perceived speed (first meaningful paint, interactivity)
and runtime smoothness while balancing maintainability.

Three layers to think in (always):


1. Build / Network — what bytes are sent (bundle size, images, fonts,
compression).
2. Load / Parse — how fast the browser parses and executes JS/CSS
(code-splitting, tree-shaking).
3. Run / Render — how often and how expensively the app updates the
DOM (React re-renders, layout/paint).

Key principle: measure first, change one thing, measure again. Always
prove improvement.

Tools & key metrics (what to use & what to read)

Core metrics to know

- LCP (Largest Contentful Paint) — perceived load speed.


- INP / FID (Interaction to Next Paint / First Input Delay) —
responsiveness.
- CLS (Cumulative Layout Shift) — visual stability.
- TTFB, FCP, TTI, TBT — server/first paint/interactive/total blocking
time.

Tools
- Chrome DevTools (Performance tab, Network tab) — record, inspect
main thread, flame charts.
- Lighthouse (DevTools or CLI) — lab audit and suggestions.
- React DevTools Profiler — which components re-render and why.
- WebPageTest / PageSpeed Insights — synthetic and field metrics.
- Bundle analyser (webpack-bundle-analyzer, rollup plugin) — inspect
bundles.
Quick how-to (DevTools):
- Open page → DevTools → Performance → Record → interact → stop.
- See Main thread, scripting tasks, long tasks (>50ms).
- Use React DevTools → Profiler → record 5–10s while interacting to
identify heavy components.

Network & build optimisations (step-by-step)


1. Measure baseline: Lighthouse score + bundle size + network waterfall.
2. Minify & compress: ensure gzip/brotli on server.
3. Enable tree-shaking (ES modules) — ensure build tool supports it
(Webpack/Rollup/Vite).
4. Code-splitting / route-based chunking
◦ Example (React lazy):

// Before: all loaded


import Dashboard from './Dashboard';

// After: split into separate chunks


const Dashboard = React.lazy(() => import('./Dashboard'));

// Usage
<Suspense fallback={<Spinner/>}>
<Dashboard />
</Suspense>

5. Dynamic imports for big libs: import charts, maps only when needed.
6. Preload / Prefetch: preload critical assets, prefetch next-route
chunks.
<link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2"
crossorigin>
<link rel="prefetch" href="/route-next.chunk.js">

7. Serve via CDN and set long cache headers for immutable assets
(Cache-Control: public, max-age=31536000, immutable).
8. Optimise bundles: remove polyfills, replace heavy libs with lighter
alternatives (e.g., date-fns instead of moment).
Asset optimisation (images, fonts, media)
• Images
- Use next-gen formats: WebP/AVIF for photos.
- Serve responsive sizes with srcset and sizes.
- Use loading="lazy" for offscreen images.
- Use a resizing CDN or sharp to generate multiple sizes.
- Example srcset:
<img src="img-800.jpg"
srcset="img-400.jpg 400w, img-800.jpg 800w, img-1200.jpg 1200w"
sizes="(max-width:600px) 400px, 800px"
loading="lazy" alt="">

• Fonts
- Subset fonts, use font-display: swap, preload critical fonts.
• Videos: lazy load, use low-res poster, provide compressed codec
options.

Run-time & DOM optimisations (browser-level)


• Reduce DOM nodes — smaller trees render faster.
• Avoid layout thrashing: read/write DOM in batches; prefer transform/
opacity for animations (GPU).
• Avoid expensive CSS selectors and heavy paint properties.
• Debounce / throttle expensive event handlers (scroll, resize, input).

Debounce example:
function debounce(fn, wait) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}

React-specific optimisation (deep, step-by-step)

Understand how React renders:


- React computes a virtual DOM diff and updates the real DOM when
necessary.
- Re-render happens when parent state/props change or context value
changes.

Stepwise checklist to optimise a React app


1. Find the slow component — use React Profiler + DevTools.
2. Isolate cause — network, render, or layout? (DevTools timeline
helps).
3. Apply appropriate pattern (memo, virtualization, code-split).
4. Measure again.

Common React patterns & examples


1. React.memo — prevents re-render of a function component when
props are shallowly equal.
const Child = React.memo(({ value }) => {
console.log('child render');
return <div>{value}</div>;
});

Only use when the child is moderately expensive to render or receives


props that rarely change.

2. useCallback — memoise functions to keep a stable reference when


passing to memoized children.
function Parent() {
const [count, setCount] = useState(0);

// Without useCallback, increment is recreated every render => child re-


renders
const increment = useCallback(() => setCount(c => c + 1), []);

return <Child onClick={increment} />;


}

Important: useCallback itself costs memory; don’t overuse—measure


first.
3. useMemo — memoise expensive computed values.
const expensive = useMemo(() => heavyCalc(data), [data]);

Don’t wrap cheap code; memoization cost must be worth the saved re-
calculation.
4. Virtualisation — for long lists, use react-window / react-virtualized.
import { FixedSizeList as List } from 'react-window';
<List height={400} itemCount={10000} itemSize={35} width={300}>
{({ index, style }) => <div style={style}>Item {index}</div>}
</List>

5. Avoid inline props/objects – creating const obj = { a: 1 } inline will


create new reference each render and may break memoization. Either
memoise the object with useMemo or move it outside.
6. Keys in lists — avoid key={index} when list changes order; use stable
ids.
7. State locality — keep the state close to where it’s used. Lifting the
state up unnecessarily can cause many re-renders.
8. Context — useful, but updating context value re-renders consumers.
Split contexts or memoise context values.
9. Batching & Transition (Concurrent features) — In modern React,
state updates are batched. Use startTransition for non-urgent
updates:

import { startTransition } from 'react';


startTransition(() => {
setBigState(newValue);
});

Concrete “before & after” mini-examples

A. Unnecessary child re-render


// Before
function Parent() {
const [count, setCount] = useState(0);
const increment = () => setCount(c => c + 1); // new fn every render
return <>
<button onClick={increment}>+</button>
<Child onClick={increment} />
</>
}

const Child = React.memo(({ onClick }) => { ... });

fix
const increment = useCallback(() => setCount(c => c + 1), []);

B. Heavy compute in render


function Component({ items }) {
const result = items.map(i => heavyCalc(i)); // expensive every render
return <List data={result} />;
}

fix
const result = useMemo(() => items.map(i => heavyCalc(i)), [items]);

Practical step-by-step workflow (apply to a real app)


1. Baseline
- Run Lighthouse & React Profiler. Save reports.
1. Pick top 2-3 bottlenecks (e.g., big JS bundle, long main-thread
tasks, long list re-renders).
2. Implement fixes one by one
- Bundle: enable code-splitting, remove unused libs.
- Long renders: memoise heavy components, virtualise lists.
- Network: compress images & text, add caching headers.
1. Re-measure (Lighthouse + Profiler). Compare.
2. If fixed, ship and monitor real users (RUM): add analytics for LCP/
TBT/CLS.
3. Repeat every sprint (small incremental wins).

Hands-on exercises (do these to internalise)


1. Measure baseline
- Pick one app/page. Record Lighthouse and React Profiler output.
1. Find a re-render hotspot
- Use React Profiler to find a component that re-renders often. Try
React.memo + useCallback and re-run profiler.
1. Virtualise a long list
- Create a 10,000-item list and measure FPS/CPU. Add react-window
and compare.
1. Code-split a route
- Convert a heavy route to React.lazy + Suspense.
1. Image optimisation
- Replace a hero image with responsive srcset and WebP; measure
LCP.
1. Critical CSS & fonts
- Preload main font, set font-display: swap, measure CLS.
1. Debounce input
- Implement a search input that debounces network requests.

Common anti-patterns & mistakes


- Premature memoisation (memo every component).
- Passing inline objects/functions to memoised children without
useMemo/useCallback.
- Using an index as a key for dynamic lists.
- Storing everything in global state (causes wide re-renders).
- Doing heavy synchronous work during render instead of async or
memoised.
- Loading large images at full resolution and resizing client-side.

Decision heuristics — when to apply what


- If bundle > 200–300 KB (gzipped) → code-split, replace heavy libs.
- If long tasks (>50ms) show in the main thread → find JS work and
memoise or defer.
- If list > ~500 visible or 1000 total → virtualise.
- If many small network requests → batch or cache.

Quick cheatsheet (actionable checklist)


1. Measure (Lighthouse + Profiler).
2. Fix the top 3 bottlenecks only.
3. Compress text (gzip/brotli) & images (WebP).
4. Code-split and lazy load non-critical code.
5. Memoise expensive renders only after measuring.
6. Virtualise large lists.
7. Localise state and avoid prop-drilling.
8. Use CDN and set caching headers.
9. Preload critical fonts and assets.
10. Re-run profiler and

React Optimisation Before/After Repo Plan

1. Code Splitting

Before
import Dashboard from './Dashboard';

export default function App() {


return <Dashboard />;
}

After
import React, { Suspense } from 'react';
const Dashboard = React.lazy(() => import('./Dashboard'));

export default function App() {


return (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
);
}

2. React.memo

Before
export default function Button({ onClick }) {
console.log('Button rendered');
return <button onClick={onClick}>Click</button>;
}

After
import React from 'react';

const Button = React.memo(function Button({ onClick }) {


console.log('Button rendered');
return <button onClick={onClick}>Click</button>;
});

export default Button;

3. useCallback

Before
export default function App() {
const handleClick = () => console.log('clicked');
return <Button onClick={handleClick} />;
}

After
import { useCallback } from 'react';

export default function App() {


const handleClick = useCallback(() => console.log('clicked'), []);
return <Button onClick={handleClick} />;
}

4. useMemo

Before
function slowFunction(num) {
console.log('Calling slow function...');
return num * 2;
}

export default function App({ num }) {


const result = slowFunction(num);
return <div>{result}</div>;
}

After
import { useMemo } from 'react';

function slowFunction(num) {
console.log('Calling slow function...');
return num * 2;
}

export default function App({ num }) {


const result = useMemo(() => slowFunction(num), [num]);
return <div>{result}</div>;
}

5. List Virtualisation

Before
export default function App() {
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
return (
<div>
{items.map((item) => (
<div key={item}>{item}</div>
))}
</div>
);
}

After
import { FixedSizeList as List } from 'react-window';

export default function App() {


const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
return (
<List
height={500}
itemCount={items.length}
itemSize={35}
width={300}
>
{({ index, style }) => <div style={style}>{items[index]}</div>}
</List>
);
}

6. Lazy Image Loading

Before
export default function ImageComponent() {
return <img src="large-image.jpg" alt="big" />;
}

After
export default function ImageComponent() {
return <img src="large-image.jpg" loading="lazy" alt="big" />;
}
7. Debouncing

Before
export default function Search() {
const handleChange = (e) => {
fetch(`/api/search?q=${e.target.value}`);
};
return <input onChange={handleChange} />;
}

After
import { useCallback } from 'react';
import debounce from 'lodash.debounce';

export default function Search() {


const handleChange = useCallback(
debounce((value) => {
fetch(`/api/search?q=${value}`);
}, 300),
[]
);

return <input onChange={(e) => handleChange(e.target.value)} />;


}

You might also like