সরাসরি পর্বে যান
পর্ব ২৩ · ৯৩-এর মধ্যে · মডিউল ৩

React performance — memo, code splitting, Suspense আর error boundary

React Performance — Measuring, memo, Code Splitting, Suspense and Error Boundaries

Taught in Bangla, this lesson measures before optimising with a production build and slowed CPU, names four common causes of slowness, shows memo needs stable props and what the React Compiler automates, keeps typing responsive with useDeferredValue and useTransition, splits code with lazy and real Vite chunks, uses Suspense boundaries and skeletons, contains failures with error boundaries, and covers images, bundle analysis and slow-phone budgets.

এই পর্বে যা শিখবেন

  • Profiler, Performance প্যানেল, long task আর INP দিয়ে একটি ধীর কাজ ধরা
  • memo, স্থির props, React Compiler, useDeferredValue আর useTransition-এর সঠিক ব্যবহার
  • lazy দিয়ে route ভাগ করা, আসল Vite chunk পড়া, আর bundle analyzer চালানো
  • Suspense boundary, skeleton আর route-প্রতি error boundary দিয়ে অ্যাপকে ভাঙন-সহনশীল করা
  • ছবি লোড করার নিয়ম আর ধীর ফোনের জন্য একটি হিসাব করা performance budget

১ · আগে মাপুন: অনুমান নয়, একটি নির্দিষ্ট কাজ

পর্ব ১৪-এ useMemo, useCallback আর compiler-এর ভিত্তি দেখেছেন। পর্ব ১৫-এ দেখেছেন render, commit আর Profiler-এর আচরণ। সেগুলো এখানে আবার পড়াব না। এই পর্বের প্রশ্ন ব্যবহারিক: “বাজারঘর” একটি পুরোনো ফোনে কেন ধীর, আর কীভাবে তাকে দ্রুত ও ভাঙন-সহনশীল করা যায়।

ঢাকার রাস্তায় বাস দেরি করছে। ড্রাইভারকে “জোরে চালান” বললে লাভ নেই, যদি আসল সমস্যা ফার্মগেট মোড়ের জট হয়। আগে দেখতে হবে সময়টা কোথায় যাচ্ছে: যাত্রী ওঠায়, জটে, না রাস্তায়। Performance-এর কাজও তাই। সময়রেখা না দেখে memo ছড়ানো মানে বাসের রং বদলে গতি বাড়ানোর চেষ্টা।

ব্রাউজারের একটিমাত্র main thread JavaScript চালায়, layout করে, ছবি আঁকে। এখানে ৫০ ms-এর বেশি চলা কাজই long task. Long task চলার সময় ব্যবহারকারীর ক্লিক অপেক্ষায় থাকে। সেই অভিজ্ঞতার মাপ INP: একটি ক্লিক, ট্যাপ বা কী চাপ থেকে পরের ফ্রেম পর্দায় আসা পর্যন্ত সময়। পুরো পাতার জীবনে সবচেয়ে খারাপ দিকের একটি মান নেওয়া হয়। web.dev-এর সীমা: ২০০ ms বা কম ভালো, ৫০০ ms-এর বেশি খারাপ, মাঝেরটুকুতে উন্নতির জায়গা আছে। আসল ব্যবহারকারীদের ৭৫তম percentile ধরে বিচার হয়।

একটি interaction-এর সময় তিন ভাগে ভাঙে। কোন ভাগ বড়, সেটিই বলে দেয় কোথায় হাত দিতে হবে।

ভাগমানেবড় হলে সন্দেহ
Input delayHandler শুরুর আগের অপেক্ষাআগে থেকে চলা long task
ProcessingHandler চলা। React-এর ক্লিকে সাধারণত render আর commit-ও এখানে পড়েবেশি বা ভারী render
Presentation delayHandler শেষ থেকে ফ্রেম আসা পর্যন্তবিশাল DOM, ভারী layout আর paint
মাপার তিন নিয়ম
  • Production build মাপুন। Development build-এ বাড়তি পরীক্ষা থাকে, তাই সময় অন্যরকম। Component-ভিত্তিক সময় দরকার হলে profiling build নিন (পর্ব ১৫)।
  • ধীর যন্ত্র সাজান। Chrome বা Edge-এর Performance প্যানেলে CPU throttling চালু করুন। আপনার ল্যাপটপ আপনার গ্রাহকের ফোন নয়।
  • একটি কাজ বাছুন। “অ্যাপ ধীর” মাপা যায় না। “তালিকায় যোগ বোতামের ক্লিক” মাপা যায়।

“বাজারঘর”-এর ২,০০০ সারির তালিকায় ঝুড়িতে পণ্য যোগ করা ধীর। Event Timing API দিয়ে সেই ক্লিক ধরি। ব্রাউজার নিজেই প্রতিটি ইভেন্টের ভাগ দেয়:

new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (!e.interactionId) continue;        // শুধু আসল ইন্টারঅ্যাকশন
    console.log(e.name, {
      total: e.duration,
      input: e.processingStart - e.startTime,
      processing: e.processingEnd - e.processingStart,
      presentation: e.startTime + e.duration - e.processingEnd,
    });
  }
}).observe({ type: 'event', durationThreshold: 16, buffered: true });

সত্যিকারের run: React 19.3.1, Vite 8.3.5 production build, headless Edge 156, CPU ৪ গুণ ধীর (CDP emulation), ১০টি ক্লিকের মধ্যমা। এটি এক মেশিনের স্থানীয় মাপ, সাধারণ ফল নয়, আর ৪ গুণ ধীর ডেস্কটপ কোনো নির্দিষ্ট ফোনের সমান নয়। সময় ৮ ms-এ গোল করা হয়, তাই তিন ভাগের যোগ মোটের সাথে হুবহু মেলে না।

plain   INP-like 240.0   input 5.8   proc 216.7   present 18.9   rows/click 2000

একটি ক্লিকে ২৪০ ms, ভালো সীমার বাইরে। ভাগগুলো দেখলেই পথ পরিষ্কার। ধাপে ধাপে দেখুন, DevTools-এর ফলকে কীভাবে কাজের সিদ্ধান্তে আনা যায়।

1function Row({ p, onAdd }) {2  const fmt = new Intl.NumberFormat('bn-BD', opts);3  return <li>{fmt.format(p.price)} …</li>;4}5<button onClick={() => onAdd(p.id)}>যোগ</button>6{products.map(p => <Row key={p.id} p={p} … />)}
ক্লিকের মাপ
Input delay
Processing
Presentation
মোট
Row render
  1. প্রস্তুতি। Production build, CPU ৪ গুণ ধীর, আর একটি মাত্র কাজ: “যোগ” বোতামে ক্লিক। Performance প্যানেলে রেকর্ড চালু।
  2. Input delay 5.8 ms. Handler প্রায় সঙ্গে সঙ্গে শুরু হয়েছে। মানে আগে থেকে কোনো long task ব্যবহারকারীকে আটকে রাখেনি। এদিকে খোঁজা বৃথা।
  3. Processing 216.7 ms. মোট সময়ের প্রায় পুরোটাই এখানে। ক্লিকে একটি সংখ্যা বদলেছে, অথচ ২,০০০টি Row চলেছে। প্রথম সন্দেহভাজন পাওয়া গেল।
  4. Presentation 18.9 ms, মোট 240 ms. আঁকার ধাপ ছোট, তাই DOM-এর আকার এখনকার আসল সমস্যা নয়। INP-এর ২০০ ms সীমা পার হয়েছে।
  5. সন্দেহ ঠিক করা। প্রতি Row-এ গড়ে 216.7 ÷ 2000 ≈ 0.11 ms. একটি ছোট সারির জন্য এটি বেশি। Flamegraph বা Bottom-Up ট্যাবে এখানেই চওড়া ফাংশন খুঁজবেন। আমাদের সন্দেহ লাইন ২: প্রতি সারিতে নতুন Intl.NumberFormat. এটি প্রমাণ নয়, তাই পরের ধাপে সরাসরি পরীক্ষা করলাম।
  6. প্রথম সমাধান: কারণটি সরান। একটি formatter বাইরে বানিয়ে সবাই ভাগ করল। Row এখনো ২,০০০ বার চলছে, তবু মোট 48 ms. memo-র একটি অক্ষরও লাগেনি।
  7. দ্বিতীয় পথ: render বাদ দিন। memo আর স্থির function দিলে Row render শূন্য, মোট 32 ms. কোন পথ নেবেন, সেটি কোডের জটিলতা দেখে ঠিক করুন, আর প্রতিটি বদলের পর আবার মাপুন।

DevTools-এ যা দেখবেন (বর্ণনা, এই পর্বে GUI চালানো হয়নি; নাম version অনুযায়ী বদলাতে পারে): Performance প্যানেলে Main সারিতে লাল কোণ চিহ্ন মানে long task. Interactions সারিতে ক্লিকের দণ্ড আর তার ভাগ দেখা যায়। Bottom-Up ট্যাবে সবচেয়ে সময়-খেকো ফাংশন ওপরে আসে। React DevTools-এর Profiler commit আর component ধরে সময় দেখায়, যা পর্ব ১৫-এ আছে।

গ্রাহকের যন্ত্রের আসল মাপের জন্য web-vitals package-এর onINP দিয়ে নিজের analytics-এ মান পাঠানো যায় (লেখার সময়ে 6.2.3; এই পর্বে চালানো হয়নি)। ল্যাবের মাপ কারণ খুঁজে দেয়, মাঠের মাপ বলে সমস্যাটি আসলে কত মানুষের। বিস্তারিত পর্ব ৮৪-এ।

২ · ধীর হওয়ার চার সাধারণ কারণ

বেশিরভাগ React অ্যাপের ধীরতা এই চারটির কোনো একটিতে পড়ে। প্রতিটির ওষুধ আলাদা। তাই আগের ধাপের মাপ দরকার।

State খুব ওপরে

Input-এর state পুরো পাতার মাথায় থাকলে প্রতি অক্ষরে পুরো পাতা চলে। State-কে যে ব্যবহার করে তার কাছে নামান (পর্ব ১৬), অথবা ভারী অংশকে children হিসেবে পাঠান (পর্ব ০৯)।

লক্ষণ: অসম্পর্কিত অংশ render

অস্থির props

Render-এর ভেতরে লেখা {}, [] আর () => … প্রতিবার নতুন reference. memo তাই “props বদলেছে” ধরে নেয়।

লক্ষণ: memo আছে, তবু render

ভারী render

Render-এর ভেতরে বড় sort, formatter বানানো, বিশাল list. কম render নয়, প্রতিটি render-কে সস্তা করা দরকার। লম্বা list-এ virtualization.

লক্ষণ: এক component-ই চওড়া

বড় bundle

ক্লিক ঠিক আছে, কিন্তু পাতা খুলতেই দেরি। JavaScript নামানো, পড়া আর চালানোর খরচ। এখানে memo কিছুই করবে না; দরকার code splitting আর বাড়তি package ছাঁটা।

লক্ষণ: প্রথম লোড ধীর

একটি অদ্ভুত কিন্তু দামি ফল: আগের মাপের সারিগুলো পাশাপাশি রাখি। সব একই ২,০০০ সারি, একই ক্লিক। দুটি আলাদা run দিলাম, যাতে স্থানীয় ওঠানামা বোঝা যায়। compiler-এর সারি দুটি পরের অংশে ব্যাখ্যা করব।

কোডRow render প্রতি ক্লিকেমোট, run ১মোট, run ২
সাধারণ, হাতে কোনো memo নেই২,০০০240 ms220 ms
memo, কিন্তু inline function২,০০০228 ms212 ms
memo + useCallback০32 ms28 ms
memo নেই, ভাগ করা formatter২,০০০48 ms48 ms
Compiler, handler-এ count + 1২,০০০48 ms48 ms
Compiler, handler-এ c => c + 1০24 ms24 ms

দ্বিতীয় সারিটি দেখুন। memo লেখা হয়েছে, অথচ লাভ শূন্য। এটিই পরের অংশের বিষয়।

৩ · memo আর স্থির props, তারপর React Compiler

memo একটি component-কে মুড়ে দেয়। Parent render হলে React আগের আর নতুন props প্রতিটি Object.is দিয়ে মেলায়। সব এক হলে child-এর ফাংশন চলে না। একটি prop আলাদা হলেই পুরো লাভ শেষ। নিজের state বা context বদলালে memo করা component তবুও চলবে।

দর্জির দোকান ভাবুন। পুরোনো মাপ মিলে গেলে নতুন করে মাপতে হয় না। কিন্তু প্রতিবার নতুন খাতা হাতে দিলে দর্জি মিলাতেই পারেন না। Inline object আর function হলো সেই নতুন খাতা। নিচের ব্লক লোড হতেই parent-কে তিনবার render করে, আর দুটি একই রকম কার্ডের render গোনে।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const counts = { unstable: 0, stable: 0 };
const Card = memo(function Card({ name, style, onAdd }) {
  counts[name]++;
  return <button style={style} onClick={onAdd}>{name}</button>;
});
const box = { padding: 6 };            // বাইরে: সবসময় একই object
let bump;
function Shop() {
  const [n, setN] = useState(0);
  bump = setN;
  const add = useCallback(() => setN(c => c + 1), []);
  useEffect(() => { console.log('ঝুড়ি', n, JSON.stringify(counts)); });
  return (
    <div>
      <p>ঝুড়িতে {n}টি</p>
      <Card name="unstable" style={{ padding: 6 }} onAdd={() => setN(c => c + 1)} />
      <Card name="stable" style={box} onAdd={add} />
    </div>
  );
}
createRoot(document.getElementById('root')).render(<Shop />);
setTimeout(() => {
  for (const v of [1, 2, 3]) ReactDOM.flushSync(() => bump(v));
}, 50);
</script>

আউটপুট:

ঝুড়ি 0 {"unstable":1,"stable":1}
ঝুড়ি 1 {"unstable":2,"stable":1}
ঝুড়ি 2 {"unstable":3,"stable":1}
ঝুড়ি 3 {"unstable":4,"stable":1}

একই memo, ফল আলাদা। unstable প্রতি parent render-এ চলেছে: ৪ বার। stable শুধু প্রথমবার। পর্দায় দুটি বোতাম হুবহু এক, আর এখানে flushSync শুধু “প্রতি update আলাদা দেখা”র জন্য। style ঠিক করে function অস্থির রাখলেও, বা উল্টোটা করলেও unstable-এর লাভ ফিরবে না; একটি prop-ই যথেষ্ট।

useCallback একা কিছু করে না। Child memo করা না থাকলে স্থির function-এর কোনো লাভ নেই। আর children দিলে JSX নিজেই প্রতিবার নতুন object, তাই memo ভাঙে। হালকা component-এ memo দিলে props মেলানোর খরচই লাভ খেয়ে ফেলতে পারে।

React Compiler কী বদলাল: আসল বিল্ড

পর্ব ১৪-এ React Compiler কী, তা দেখেছেন। এবার একই “বাজারঘর” কোড compiler সহ build করে মাপলাম। @vitejs/plugin-react 6-এ Babel option সরানো হয়েছে, তাই আলাদা plugin লাগে। সত্যিকারের config, compiler 1.0.0, @babel/core 8.0.10, @rolldown/plugin-babel 0.2.4:

import { defineConfig } from 'vite';
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
import babel from '@rolldown/plugin-babel';

export default defineConfig({
  plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
});

আগের টেবিলের শেষ দুই সারি এই build-এর। হাতে একটিও memo নেই। Handler-এ c => c + 1 থাকলে render শূন্য, মোট 24 ms, হাতে লেখা সেরা সংস্করণের সমান বা ভালো। Build ফাইলে (minify বন্ধ করে) কারণটি দেখা গেল:

function PlainUpd() {
	const $ = (0, import_compiler_runtime.c)(5);
	const [count, setCount] = (0, import_react.useState)(0);
	…
	let t1;
	if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
		t1 = … ("ul", { children: products.map((p) => … (Row, {
			p,
			onAdd: () => setCount(_temp)
		}, p.id)) });
		$[2] = t1;
	} else t1 = $[2];

পুরো ul একবার বানিয়ে জমা রেখেছে। Handler নিজেই _temp নামে বাইরে সরে গেছে। পরের render-এ একই object, তাই React পুরো subtree বাদ দেয়।

কিন্তু অন্য সারিটি শিক্ষণীয়। Handler যখন setCount(count + 1) লেখে, function-টি count-এর ওপর নির্ভর করে। তাই প্রতি ক্লিকে সেটি সত্যিই বদলায়, আর ২,০০০ Row চলে। Compiler অর্থ বদলায় না, তাই সে এটি ঠিক করতে পারে না। তবু 48 ms এল, কারণ build ফাইলে দেখা গেল সে প্রতি Row-এ Intl.NumberFormat ডাকটি জমা রেখেছে। তাই কথাটি হলো: compiler অনেক হাতের কাজ বাঁচায়, মাপা বাঁচায় না। আর সে যা জমায় তা build ফাইল পড়েই নিশ্চিত হওয়া যায়।

৪ · ভারী তালিকার filter: useDeferredValue আর useTransition

পর্ব ১৪-এ useDeferredValue আর useTransition-এর তুলনা, আর পর্ব ১৫-এ time slicing দেখেছেন। এখন একটি পূর্ণ সমস্যা। “বাজারঘর”-এর খোঁজ বাক্সে টাইপ করলে ২,০০০ পণ্য থেকে ফল বদলায়। মেমো এখানে যথেষ্ট নয়, কারণ প্রতি অক্ষরে props সত্যিই বদলায়। কাজটি অনিবার্য; প্রশ্ন শুধু কখন হবে।

চায়ের দোকানে ক্রেতা অর্ডার দিচ্ছেন। দোকানি আগে “জি, নিলাম” বলেন, তারপর চা বানান। ক্রেতা অর্ডার বদলালে আধা-বানানো চা ফেলে নতুনটি ধরেন। “জি, নিলাম” হলো input-এর অক্ষর, চা হলো তালিকা। useDeferredValue ঠিক এই ভাগটি করে। কাজ কমে না, ক্রম বদলায়।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const names = ['dal', 'chal', 'tel', 'chini'];
const items = Array.from({ length: 100 }, (_, i) => names[i % 4] + '-' + i);
let rows = 0;
function Row({ text }) {               // ইচ্ছা করে ধীর component
  rows++;
  const end = performance.now() + 0.5;
  while (performance.now() < end);
  return <li>{text}</li>;
}
const List = memo(({ q }) =>
  <ul>{items.filter(x => x.includes(q)).map(x => <Row key={x} text={x} />)}</ul>);
const setters = [];
function Box({ defer }) {
  const [text, setText] = useState('');
  const late = useDeferredValue(text);
  setters.push(setText);
  return <><input value={text} onChange={e => setText(e.target.value)} />
    <List q={defer ? late : text} /></>;
}
const type = (set, mark) => { rows = 0;
  for (const t of ['c', 'ch', 'chi']) ReactDOM.flushSync(() => set(t));
  setTimeout(() => console.log(mark, 'Row render:', rows), 150); };
createRoot(document.getElementById('root')).render(
  <div><Box defer={false} /><Box defer={true} /></div>);
setTimeout(() => { type(setters[0], 'সাধারণ'); setTimeout(() => type(setters[1], 'deferred'), 200); }, 200);
</script>

আউটপুট:

সাধারণ Row render: 125
deferred Row render: 25

দুটি বাক্সেই “chi” লেখা হলো, এক টানে তিন অক্ষর। সাধারণ বাক্সে তিনটি তালিকাই পুরো render: 50 + 50 + 25 = 125. Deferred বাক্সে জরুরি render-গুলো memo করা তালিকা ছুঁয়েও দেখেনি, শুধু শেষ মানের ২৫টি সারি চলেছে। এখন পাতায় নিজে টাইপ করে দেখুন, আর ধীরতা বোঝাতে 0.5-কে 5 করে দেখুন। সব অক্ষর এক টানে আসায় সংখ্যাটি এত পরিষ্কার; আসল টাইপিংয়ে ফাঁকে ফাঁকে কিছু পুরোনো মানও render হতে পারে।

এবার আসল build, আসল কী-চাপ: chini, অক্ষরের ফাঁক 100 ms, ২,০০০ পণ্য, প্রতি সারিতে নতুন formatter. একই পরিবেশ, প্রতিটি ধরন ৩ বার চালানো। “সবচেয়ে ধীর কী-চাপ” হলো Event Timing-এর মান। সংখ্যাগুলো এক মেশিনের স্থানীয় মাপ।

ধরনসবচেয়ে ধীর কী-চাপLong task (৫ অক্ষরে)মোট Row render
সাধারণ184–264 ms৫টি৬,৪০০
useDeferredValue, memo ছাড়া208–256 ms৫টিপ্রায় ১৫,৫০০
useDeferredValue + memo48–64 ms০প্রায় ৫,৩০০–৫,৬০০
useTransition + memo40–56 ms০৩,৪০০–৫,৪০০

দ্বিতীয় সারিটি সতর্কবার্তা। memo ছাড়া useDeferredValue দিলে অবস্থা আরও খারাপ হয়েছে। প্রতিটি অক্ষরে তালিকা দুবার চলেছে: একবার জরুরি render-এ, একবার পেছনে। তাই render প্রায় আড়াই গুণ। React-এর docs-ও বলে, এই কৌশলে memo আবশ্যক। সঠিকভাবে লাগালে long task শূন্যে নেমেছে, যদিও মোট কাজ প্রায় একই। ব্যবহারকারীর কাছে বাক্সটি হালকা থাকে, আর ফল একটু পরে আসে।

অবস্থাবেছে নিন
মানটি prop হয়ে এসেছে, set আপনার হাতে নেইuseDeferredValue
set আপনার হাতে, “চলছে” চিহ্ন দেখাতে চানuseTransition-এর isPending
প্রতি অক্ষরে network request যাচ্ছেDebounce; এই দুটি request কমায় না
তালিকা এত বড় যে একবার render-ই ভারীVirtualization বা pagination

Virtualization-এর ছোট স্মরণ: পর্ব ১১-এ হিসাব করেছিলেন, ১০,০০০ সারিতে শুধু ২৫টি আঁকলে ৪০,০০১-এর জায়গায় ১০১টি element থাকে। Virtualization render আর DOM দুটোই কমায়। এখানে যে পদ্ধতি, তা শুধু সময়ে ছড়িয়ে দেয়। দুটি আলাদা হাতিয়ার, একসাথেও চলে। মনে রাখবেন, আঁকা হয়নি এমন সারি ব্রাউজারের Ctrl+F খুঁজে পায় না।

৫ · Code splitting: lazy আর আসল Vite chunk

এতক্ষণ ক্লিকের গতি দেখলাম। এবার পাতা খোলার গতি। অ্যাপের সব কোড এক ফাইলে থাকলে, যিনি শুধু ডাল কিনতে এসেছেন তিনিও অ্যাডমিনের চার্টের কোড নামান। Code splitting কোডকে কয়েকটি chunk-এ ভাগ করে, দরকার হলে তবেই আনে। ঠিক যেন গার্মেন্টসের পুরো গুদাম ট্রাকে না তুলে আজকের অর্ডারটুকু পাঠানো।

React-এ পথ দুটি জিনিসে তৈরি। JavaScript-এর dynamic import (import(), module আর Promise-এর ভিত্তিতে) আর React-এর lazy:

import { lazy } from 'react';

// component-এর বাইরে, ফাইলের ওপরে। default export হতে হবে।
export const Cart = lazy(() => import('./routes/Cart.jsx'));
export const Admin = lazy(() => import('./routes/Admin.jsx'));

React প্রথমবার Admin render করার চেষ্টা করলে তবেই import() চলে, আর ফল জমা থাকে, তাই ফাইল একবারই আসে। lazy component-এর ভেতরে লিখলে প্রতি render-এ নতুন component হয়ে state হারায়। Route-এর সাধারণ ব্যবহার পর্ব ২০-এ, এখানে শুধু bundle-এর প্রভাব।

“বাজারঘর”-এর ছোট সংস্করণ চারটি route নিয়ে build করলাম: হোম, পণ্য, ঝুড়ি, আর recharts (লেখার সময়ে 3.10.1) ব্যবহার করা অ্যাডমিন চার্ট। হোমে ইচ্ছা করেই একটি পরিচিত ভুল রেখেছি: শুধু debounce-এর জন্য পুরো lodash. সত্যিকারের run, Vite 8.3.5, React Router 8.5.1. প্রথমে সব route সাধারণ import-এ:

$ npx vite build
dist/index.html                  0.20 kB │ gzip:   0.16 kB
dist/assets/index-BuZp6n3r.js  665.47 kB │ gzip: 205.62 kB

(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application

একই কোড, চারটি route lazy করে:

$ npx vite build
dist/index.html                        0.28 kB │ gzip:  0.21 kB
dist/assets/Product-DKG2zWx5.js        0.19 kB │ gzip:  0.19 kB
dist/assets/Cart-DRcNoLy2.js           0.22 kB │ gzip:  0.21 kB
dist/assets/jsx-runtime-CeWad5y0.js    8.86 kB │ gzip:  3.38 kB
dist/assets/Home-Dg9zIpO4.js          71.69 kB │ gzip: 26.33 kB
dist/assets/index-BFnGO9Mb.js        249.97 kB │ gzip: 79.62 kB
dist/assets/Admin-CQZsPQtG.js        336.04 kB │ gzip: 98.07 kB

ফাইলের নামের মাঝের অংশটি content hash. Admin ও recharts এখন আলাদা, আর হোমে সে আসবেই না। হোমের জন্য নামে index + jsx-runtime + Home = 79.62 + 3.38 + 26.33 = 109.33 kB (gzip), আগে ছিল 205.62 kB. মানে প্রায় ৪৭% কম।

পাতায় আসলে কত সময় বাঁচল?

বাইট কমা আর সময় কমা এক কথা নয়। তাই ঠান্ডা cache-এ মাপলাম। ধরে নেওয়া নেটওয়ার্ক: 150 ms RTT, 200 kB/s (প্রায় 1.6 Mbps); CPU ৪ গুণ ধীর; ৭ বারের মধ্যমা, কয়েক দফা চালানো। স্থানীয় gzip server, এক মেশিনের মাপ।

Buildহোমের JS (gzip)হোমের তালিকা DOM-এঅ্যাডমিন ক্লিক থেকে চার্ট
সব একসাথে203 kBপ্রায় 1600 msপ্রায় 160 ms
সব route lazy108 kBপ্রায় 1240 msপ্রায় 840 ms
সব lazy, lodash বাদ83 kBপ্রায় 1200–1240 msপ্রায় 840 ms
হোম সাধারণ import, বাকি lazy, lodash বাদ82 kBপ্রায় 860 msপ্রায় 850 ms

তিনটি শিক্ষা, প্রতিটি মাপ থেকে এসেছে।

  • দাম আছে। অ্যাডমিন চার্ট 160 ms থেকে 840 ms হলো। আগে যে কোড নামানো ছিল, এখন তা ক্লিকের পরে আসে। যে পাতায় প্রায় সবাই যান, সেটি আলাদা করে লাভ কম।
  • প্রথম পাতা lazy করবেন না। Home নিজে lazy হলে আগে মূল chunk আসে, চলে, তারপর আরেকটি request. আমাদের মাপে তা প্রায় 380 ms বেশি।
  • 25 kB কমলেও সময় বদলায়নি কেন? সারি ২ আর ৩-এ তালিকা প্রায় একই সময়ে এসেছে, অথচ ছোট Home chunk প্রায় 100–150 ms আগে পৌঁছেছিল। সম্ভাব্য কারণ: React-এর docs বলে, লুকানো content সে শেষ প্রকাশের পর অন্তত 300 ms ফাঁকে প্রকাশ করে। আমরা এটি আলাদা করে প্রমাণ করিনি, তাই অনুমান হিসেবেই নিন।

অপেক্ষা কমাতে, ব্যবহারকারী লিংকে আঙুল রাখার আগেই chunk আনা যায়। onPointerEnter-এ একই import() ডাকলেই চলে; ব্রাউজার module জমা রাখে, ক্লিকের সময় আর নামাতে হয় না। মাথায় রাখুন, ফোনে hover নেই, আর ধীর ডেটা-প্যাকে আগাম নামানো গ্রাহকের টাকা খরচ করে।

৬ · Suspense boundary আর skeleton

Suspense একটি অংশকে বলে, “নিচের কেউ অপেক্ষা করলে এই fallback দেখাও।” lazy কোড, আর use-এ পড়া Promise এটিকে চালু করে। মনে রাখুন, Effect-এর ভেতরে করা fetch Suspense টের পায় না। নিচের ব্লকে ৪০০ ms-এর “কোড” আনা সাজানো হয়েছে; লোড হতেই খোলে, বন্ধ হয়, আবার খোলে।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
let loads = 0, toggle;
function CartPage() {
  useEffect(() => { console.log('ঝুড়ির পাতা পর্দায়'); }, []);
  return <p>ঝুড়ির পাতা</p>;
}
const Cart = lazy(() => {
  console.log('chunk আনা শুরু, বার', ++loads);
  return new Promise(done => setTimeout(() => done({ default: CartPage }), 150));
});
function Skeleton() {
  useEffect(() => {
    console.log('skeleton দেখা গেল');
    return () => console.log('skeleton সরল');
  }, []);
  return <div style={{ height: 40, background: '#e2e8f0' }} />;
}
function App() {
  const [open, setOpen] = useState(false);
  toggle = setOpen;
  return <Suspense fallback={<Skeleton />}>{open ? <Cart /> : <p>হোম</p>}</Suspense>;
}
createRoot(document.getElementById('root')).render(<App />);
setTimeout(() => toggle(true), 20);
setTimeout(() => toggle(false), 450);
setTimeout(() => { toggle(true); console.log('আবার খোলা'); }, 500);
</script>

আউটপুট:

chunk আনা শুরু, বার 1
skeleton দেখা গেল
skeleton সরল
ঝুড়ির পাতা পর্দায়
আবার খোলা
ঝুড়ির পাতা পর্দায়

দ্বিতীয়বার chunk আনা শুরু লেখা নেই, skeleton-ও নেই। lazy ফল মনে রাখে। তাই একটি route-এ অপেক্ষা সাধারণত একবারই।

Skeleton হলো পাতার ধূসর কাঠামো। ঘুরন্ত spinner শুধু বলে “অপেক্ষা করুন”; skeleton বলে “এখানে এমন কিছু আসছে”। আকার আসল content-এর সমান রাখলে পাতা লাফায় না, মানে CLS বাড়ে না। Boundary-র জায়গাটিই আসল নকশা। ধরুন পণ্যের পাতা: শিরোনাম আর দাম আগে, রিভিউ পরে।

<Suspense fallback={<PageSkeleton />}>
  <ProductInfo />                       {/* একসাথে আসে */}
  <BuyBox />
  <Suspense fallback={<ReviewsSkeleton />}>
    <Reviews />                         {/* ধীর হলেও ওপরেরটা আটকায় না */}
  </Suspense>
</Suspense>

একই boundary-র ভাইবোনেরা একসাথে আসে। প্রতিটি component-এ আলাদা boundary দিলে পাতায় একের পর এক ঝলক লাগে। Boundary বসান ব্যবহারকারীর দেখার ক্রম অনুযায়ী, কোডের গঠন অনুযায়ী নয়।

আসল run-এর একটি চমক: fallback দেখা গেল না

আগের মাপে অ্যাডমিন ক্লিকে একবারও “লোড হচ্ছে…” পর্দায় আসেনি, পুরো 840 ms পুরোনো পাতা ঝুলে ছিল। কারণ BrowserRouter navigation startTransition-এ মোড়ে, আর transition আগে দেখানো content লুকায় না। React-এর এই আচরণ নথিবদ্ধ। সমাধানও নথিতে: ভিন্ন content হলে boundary-তে key দিন। key বদলালে সেটি নতুন boundary, তাই fallback সঙ্গে সঙ্গে আসে। আমাদের run-এ fallback এল ক্লিকের প্রায় 16–24 ms পরে।

function Page({ children }) {
  const { pathname } = useLocation();
  return <Suspense key={pathname} fallback={<PageSkeleton />}>{children}</Suspense>;
}

কোনটি ভালো, তা পাতার ওপর নির্ভর করে। দ্রুত chunk-এ পুরোনো পাতা ধরে রাখলে ঝলক কম হয়। ধীর নেটওয়ার্কে কিছু না দেখালে মানুষ ভাবেন ক্লিক হয়নি। তখন skeleton বা ওপরের সরু লোডিং-রেখা ভালো।

  1. ক্লিক/admin0 ms
  2. lazyimport()suspend
  3. Fallbackskeletonপ্রায় 18 ms
  4. ChunkAdmin-….js97 kB
  5. চার্টcommitপ্রায় 840 ms
পর্দায়হোম পাতা
URL বদলাল: /admin
React নতুন route render করছে
React-এর ভেতরেrender থেমে গেল
<Admin /> → lazy → load() প্রথমবার ডাকা হলো
Promise এখনো pending
নিকটতম Suspense খোঁজা শুরু
পর্দায়key বদলেছে, তাই নতুন boundary
মেনু       থাকল
PageSkeleton দেখা গেল
হোম পাতা সরে গেল
নেটওয়ার্কধরে নেওয়া 200 kB/s
GET Admin-CQZsPQtG.js
Body 97,095 bytes (gzip)
এক run-এ সময়: 656 ms
পর্দায়কাজ শেষ
Promise resolved, .default = Admin
React আবার render করল
Skeleton সরে চার্ট এল
পরের ক্লিকে: chunk আর আনতে হবে না
  1. ক্লিক। ব্যবহারকারী “অ্যাডমিন” চাপলেন। Router URL বদলাল, React নতুন route render শুরু করল। Admin-এর কোড এখনো ব্রাউজারে নেই।
  2. lazy থামল। React <Admin /> render করতে গিয়ে প্রথমবার load() ডাকল, পেল pending Promise. Component suspend করল, ওপরে নিকটতম Suspense খোঁজা হলো।
  3. Fallback. key-সহ boundary নতুন, তাই ক্লিকের প্রায় 18 ms পরে skeleton পর্দায়। মেনু boundary-র বাইরে, তাই সে নড়েনি। key না থাকলে এখানে পুরোনো পাতাই থাকত।
  4. Chunk নামছে। 97,095 bytes gzip, ধরে নেওয়া 200 kB/s-এ প্রায় 0.49 s, সাথে latency. একটি run-এ পুরো request 656 ms নিয়েছে।
  5. চার্ট। Chunk চলল, Promise resolve হলো, React আবার render করে commit করল। মোট প্রায় 840 ms. পরেরবার lazy-র জমা ফল থেকে সঙ্গে সঙ্গে আসবে।

৭ · Error boundary: একটি অংশ ভাঙলে পুরো দোকান নয়

Suspense অপেক্ষা সামলায়, error boundary ব্যর্থতা সামলায়। React-এর docs স্পষ্ট বলে, render-এর সময় error হলে এবং কেউ না ধরলে React তার UI পর্দা থেকে সরিয়ে দেয়। মানে রিভিউর একটি ছোট ভুলে পুরো “বাজারঘর” সাদা পাতা। ক্রেতার ঝুড়ি, মেনু, কিছুই থাকে না।

বাড়ির বৈদ্যুতিক বোর্ডে প্রতি ঘরের আলাদা ব্রেকার থাকে। রান্নাঘরে শর্ট হলে সেই ঘরের ব্রেকার পড়ে, বাকি বাড়ি জ্বলে থাকে। একটিমাত্র মেইন সুইচ থাকলে পুরো বাড়ি অন্ধকার। Boundary হলো সেই ব্রেকার। যত কাছে, ক্ষতি তত ছোট।

Boundary আজও class component ছাড়া লেখা যায় না। Docs অনুযায়ী এর function সংস্করণ নেই। দুটি method নিয়ে এটি হয়:

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
class Boundary extends Component {
  state = { error: null };
  static getDerivedStateFromError(error) { return { error }; }
  componentDidCatch(error) {
    console.log(this.props.name, 'ধরেছে:', error.message);
  }
  render() {
    if (this.state.error) return <p>{this.props.name}: দেখানো গেল না</p>;
    return this.props.children;
  }
}
function Reviews({ list }) {
  return <p>গড় রেটিং {list.length}</p>;   // list না এলে throw
}
function Page() {
  return (
    <Boundary name="পাতা">
      <h3>মসুর ডাল</h3>
      <Boundary name="রিভিউ"><Reviews /></Boundary>
      <button>ঝুড়িতে যোগ করুন</button>
    </Boundary>
  );
}
createRoot(document.getElementById('root'), { onCaughtError() {} }).render(<Page />);
setTimeout(() => console.log('পর্দা:', document.getElementById('root').innerText), 50);
</script>

আউটপুট:

রিভিউ ধরেছে: Cannot read properties of undefined (reading 'length')
পর্দা: মসুর ডাল

রিভিউ: দেখানো গেল না

ঝুড়িতে যোগ করুন

শুধু রিভিউ ভেঙেছে। পণ্যের নাম আর কেনার বোতাম টিকে আছে, আর বাইরের “পাতা” boundary কিছুই টের পায়নি। getDerivedStateFromError render ধাপে চলে, তাই সেটি হবে pure; সে শুধু পরের state বলে। componentDidCatch commit-এর পর চলে, log পাঠানোর জায়গা এটি। onCaughtError React 19-এর root option, এখানে React-এর নিজের console লেখা বন্ধ করতে দিয়েছি; আসল অ্যাপে এখান থেকেও রিপোর্ট পাঠানো যায়।

Component treerender চলছে
App
├ Nav
└ Boundary[পাতা]
  └ ProductPage
    ├ ProductInfo   ✓
    ├ Boundary[রিভিউ]
    │ └ Reviews   ← এখন চলছে
    └ BuyBox
Component treethrow
Boundary[রিভিউ]
└ Reviews
  TypeError: reading 'length'
এই render বাতিল, DOM এখনো বদলায়নি
React-এর খোঁজওপরের দিকে হাঁটা
Reviews          boundary নয়
Boundary[রিভিউ]  ✓ এটিই নিকটতম
Boundary[পাতা]   খোঁজা হলো না
Render ধাপfallback বানানো
getDerivedStateFromError(error)
→ state = { error }
→ render(): <p>রিভিউ: দেখানো গেল না</p>
Commit ধাপপর্দায়
ProductInfo   ✓ আছে
“রিভিউ: দেখানো গেল না”
BuyBox        ✓ আছে
componentDidCatch → log পাঠানো
যদি রিভিউ boundary না থাকতবড় ক্ষতি
Reviews → Boundary[পাতা]
পুরো পাতার fallback
Nav          ✓ টিকে থাকে
কোনো boundary না থাকলে: পুরো অ্যাপ পর্দা থেকে সরে যেত
  1. Render চলছে। React tree ধরে নামছে। ProductInfo ঠিকমতো শেষ, এখন Reviews চলছে।
  2. Throw. list আসেনি, তাই list.length error দিল। এটি render-এর সময় ঘটেছে, তাই boundary ধরতে পারবে। পর্দায় এখনো কিছু বদলায়নি।
  3. ওপরে খোঁজা। React tree ধরে ওপরে হাঁটে এবং প্রথম boundary-তে থামে। বাইরের “পাতা” boundary পর্যন্ত যাওয়ার দরকার পড়ল না।
  4. Fallback ঠিক হলো। getDerivedStateFromError নতুন state দিল, আর boundary-র render fallback ফেরত দিল।
  5. Commit. শুধু রিভিউর জায়গাটুকু বদলাল, বাকি পাতা অক্ষত। এরপর componentDidCatch log পাঠাল।
  6. নিকটতম boundary না থাকলে। Error “পাতা” boundary-তে যেত, পুরো পাতা যেত, মেনু টিকত। কোনো boundary না থাকলে পুরো অ্যাপ সরে যেত। তাই boundary কোথায় বসছে, তা ঠিক করে ক্ষতির আকার।

Boundary যা ধরে না: event handler-এর error, setTimeout-এর মতো async কোড, server rendering, আর boundary-র নিজের ভেতরের error. ক্লিক handler-এ try/catch লাগবে। React 19-এ useTransition-এর startTransition-এ দেওয়া ফাংশনের error boundary ধরে, এটি ব্যতিক্রম।

react-error-boundary আর route-প্রতি সহনশীলতা

নিজে class না লিখে অনেকে react-error-boundary (লেখার সময়ে 6.1.6) নেন; React-এর docs-ও এটিকে উদাহরণ হিসেবে উল্লেখ করে। এটি বাধ্যতামূলক নয়, আপনার ছোট class-ও একই কাজ করে। বেছে নেওয়া সুবিধার প্রশ্ন, আর API version ধরে বদলাতে পারে, তাই নিজের version-এর নথি দেখুন। ৬ নম্বর version শুধু ES Module-এ আসে।

import { ErrorBoundary, getErrorMessage } from 'react-error-boundary';

function RouteError({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>পাতাটি দেখানো গেল না: {getErrorMessage(error)}</p>
      <button onClick={resetErrorBoundary}>আবার চেষ্টা করুন</button>
    </div>
  );
}
export function RouteShell({ children }) {
  const { pathname } = useLocation();
  return (
    <ErrorBoundary FallbackComponent={RouteError} resetKeys={[pathname]}
      onError={(error, info) => reportError(error, info)}>
      <Suspense key={pathname} fallback={<PageSkeleton />}>{children}</Suspense>
    </ErrorBoundary>
  );
}

resetKeys-এ pathname দিলে অন্য route-এ গেলে error মুছে যায়, ব্যবহারকারী আটকে থাকেন না। reportError এখানে আপনার নিজের log ফাংশনের নাম। কোনো গোপন তথ্য বা পূর্ণ error ব্যবহারকারীকে দেখানো ঠিক নয়; আসল অ্যাপে সাধারণ বার্তা দিন, বিস্তারিত log-এ পাঠান।

একটি route-এর সহনশীল গঠন

Nav stays; ErrorBoundary and Suspense wrap each route

Route-এর ভেতরে ErrorBoundary, Suspense আর পাতার স্তর App shell Nav: সবসময় থাকে ErrorBoundary: route Suspense: skeleton ProductPage নাম, দাম, BuyBox ErrorBoundary: রিভিউ Suspense Reviews: ধীর, ভাঙতে পারে ভাঙলে কেনা আটকায় না
ছবি ২৩.১ — ভেতরের boundary ছোট ক্ষতি আটকায়, বাইরেরটি বড় ক্ষতি। মেনু সবার বাইরে, তাই ব্যবহারকারী সবসময় অন্য পাতায় যেতে পারেন।

Chunk নামতে ব্যর্থ হলে: একটি আসল ফাঁদ

lazy-র Promise reject হলে React সেটি নিকটতম error boundary-তে throw করে। নেটওয়ার্ক কাটলে, অথবা নতুন deploy-এ পুরোনো chunk মুছে গেলে এমন হয়। ফাঁদটি “আবার চেষ্টা করুন” বোতামে। আমরা Report chunk-এর প্রথম request ইচ্ছা করে ব্যর্থ করে পরীক্ষা করলাম। সত্যিকারের run, Edge 156, Vite 8.3.5 build, react-error-boundary 6.1.6. নিচে ফল সংক্ষেপে সাজানো, বাংলা লাইনগুলো আমাদের script-এর:

vite:preloadError fired
onError: Failed to fetch dynamically imported module: …/assets/Report-BXfUWlhL.js
মেনু: মেনু ঠিক আছে               ← boundary কাজ করেছে
নেটওয়ার্ক ফিরল, "আবার চেষ্টা" চাপা হলো:
report পর্দায়? 0   একই error    server-এ request: মোট 1
reset-এ নতুন lazy() বানিয়ে আবার চেষ্টা:
report পর্দায়? 0   একই error    server-এ request: মোট 1
পাতা reload করে:
report পর্দায়? 1

দুটি জিনিস ব্যর্থ ফল মনে রেখেছে। lazy নিজের Promise জমা রাখে, তাই reset-এ কিছু বদলায় না। আর নতুন lazy() বানিয়েও এই browser-এ request-ই যায়নি: একই URL-এর ব্যর্থ import() সে আগের ফলই দিয়েছে। Abort আর 404, দুই ক্ষেত্রেই একই ফল। অর্থাৎ এই ক্ষেত্রে আমাদের পরীক্ষায় সৎ সমাধান ছিল পাতা reload.

Vite-এর docs এজন্য vite:preloadError ইভেন্ট দেয়, আর উদাহরণে পাতা reload করে। একটি সতর্কতা যোগ করুন: chunk সত্যিই না থাকলে reload-এর চক্র চলতে পারে, তাই sessionStorage-এ একবারের চিহ্ন রাখা ভালো। Vite HTML-এ Cache-Control: no-cache রাখতেও বলে, নইলে পুরোনো HTML পুরোনো chunk খুঁজবে।

window.addEventListener('vite:preloadError', () => {
  if (sessionStorage.getItem('chunk-reloaded')) return;  // একবারই
  sessionStorage.setItem('chunk-reloaded', '1');
  window.location.reload();
});

৮ · ছবি: প্রায়ই সবচেয়ে বড় বোঝা

অনলাইন দোকানে সবচেয়ে বেশি বাইট অনেক সময় JavaScript নয়, ছবি। “বাজারঘর”-এর হোমের বড় ব্যানার সম্ভবত LCP element. ছবির ভিত্তি আছে responsive images পর্বে; এখানে React-এ চারটি নিয়ম।

// ওপরের ব্যানার: LCP-র প্রার্থী
<img src="/img/banner-800.webp"
     srcSet="/img/banner-400.webp 400w, /img/banner-800.webp 800w"
     sizes="100vw" width="800" height="400"
     alt="ঈদের বিশেষ ছাড়" fetchPriority="high" />

// তালিকার নিচের দিকের ছবি
<img src={p.thumb} width="200" height="200" alt={p.name}
     loading="lazy" decoding="async" />
  • LCP ছবিতে loading="lazy" নয়। এটি সবচেয়ে প্রচলিত ভুল। AI প্রায়ই সব ছবিতে বসিয়ে দেয়, আর আসল ছবিটিই দেরি করে।
  • fetchpriority শুধু ১–২টি ছবিতে। সবাইকে জরুরি বললে কেউ জরুরি নয়। স্বাভাবিক ছবিতে attribute-টি বাদ দিন। JSX-এ লিখুন fetchPriority.
  • width আর height দিন। ব্রাউজার আগে থেকে জায়গা রাখে, তাই পাতা লাফায় না।
  • srcset দিন। ব্রাউজার পর্দা অনুযায়ী মাপ বাছে। WebP বা AVIF-ও বিবেচনা করুন।

হিসাব: ৩৬০ CSS px চওড়া ফোন, device pixel ratio ২, মানে দরকার ৭২০ px চওড়া ছবি। আপনি পাঠালেন ১৬০০ px. পিক্সেল বেশি (১৬০০ ÷ ৭২০)² ≈ ৪.৯ গুণ। ধরে নিই ১৬০০ px ছবি 400 kB আর ৭২০ px ছবি 90 kB. 200 kB/s-এ 2 s বনাম 0.45 s. শুধু এই একটি ছবিতেই আমাদের পুরো JavaScript-এর চেয়ে বড় ক্ষতি। ছবির সাইজগুলো ধরে নেওয়া, মাপা নয়। Next.js-এর image component আর অন্যান্য বিষয় পর্ব ৮৪-এ।

৯ · Bundle বিশ্লেষণ আর ধীর ফোনের বাজেট

Build-এর শেষে শুধু chunk-এর আকার দেখা যায়, ভেতরে কী আছে তা নয়। rollup-plugin-visualizer (লেখার সময়ে 7.1.1) একটি treemap বানায়: প্রতিটি module একটি বাক্স, বড় বাক্স মানে বড় কোড। Vite 8-এর সাথেও আমাদের run-এ এটি চলেছে।

import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [react(), visualizer({ filename: 'stats.html', gzipSize: true })],
});

npx vite build চালালে stats.html তৈরি হয়, ব্রাউজারে খুলুন। এখানে একই ডেটা template: 'raw-data' দিয়ে JSON-এ নিয়ে package ধরে যোগ করেছি। সত্যিকারের run, বড়গুলো (rendered মানে minify-এর আগের আকার, তাই build-এর লাইন থেকে বড়):

assets/index-BFnGO9Mb.js
   react-dom      524.1 kB rendered    99.0 kB gz
   react-router    80.3 kB rendered    24.3 kB gz
   scheduler        9.0 kB rendered     2.4 kB gz
assets/Home-Dg9zIpO4.js
   lodash         177.2 kB rendered    38.3 kB gz
   (app src)        0.9 kB rendered     0.5 kB gz
assets/Admin-CQZsPQtG.js
   recharts       469.0 kB rendered   149.0 kB gz
   es-toolkit      30.0 kB rendered    13.6 kB gz
   d3-scale        26.6 kB rendered    10.1 kB gz

ধরা পড়ল। Home-এর নিজের কোড 0.9 kB, অথচ lodash 177 kB, একটি ছোট debounce-এর জন্য। import _ from 'lodash' পুরো library আনে, কারণ tree shaking এই CommonJS-ধাঁচের package থেকে অব্যবহৃত অংশ বাদ দিতে পারে না। পর্ব ১৪-এর মতো তিন লাইনের নিজের debounce দিতেই Home chunk 26.33 kB থেকে 0.42 kB (gzip) হলো। React আর router-এর 80 kB সব পাতায় লাগবেই, এটিই ভিত্তির খরচ।

হিসাব করা বাজেট

“দ্রুত করুন” কোনো লক্ষ্য নয়। Performance budget হলো সংখ্যায় লেখা সীমা। ধরুন লক্ষ্য: ধীর ফোনে ৩ সেকেন্ডে প্রথম পাতা কাজের। এখানে সব সংখ্যা ধরে নেওয়া।

  1. নেটওয়ার্ক 200 kB/s, RTT 150 ms. সংযোগে ৩ RTT আর HTML-এ ১ RTT: ৪ × 150 = 600 ms. বাকি 3000 − 600 = 2400 ms.
  2. নামানোর খরচ: 1 kB ÷ 200 kB/s = 5 ms/kB.
  3. চালানোর খরচ: আমাদের run-এ 203 kB gzip-এর chunk পৌঁছানোর প্রায় 200 ms পর তালিকা এল, ≈ 1 ms/kB (৪ গুণ ধীর CPU). আসল সস্তা ফোন আরও ধীর হতে পারে, তাই নিরাপত্তার জন্য ধরি 3 ms/kB.
  4. মোট 8 ms/kB, তাই 2400 ÷ 8 = 300 kB সব সম্পদ মিলিয়ে।
  5. অর্ধেক ছবি, CSS আর font-এর জন্য রাখলে প্রথম পাতার JavaScript 150 kB (gzip).

আমাদের 82 kB-র build এই সীমার ভেতরে। প্রথম 205 kB-র build বাইরে ছিল। তবে 3 ms/kB একটি অনুমান, তাই নিজের লক্ষ্য ফোনে মেপে সংখ্যাটি বসান।

মাপআমাদের লক্ষ্যউৎস
INP (P75)≤ ২০০ msWeb Vitals-এর “ভালো” সীমা
LCP (P75)≤ ২.৫ sWeb Vitals-এর “ভালো” সীমা
CLS (P75)≤ ০.১Web Vitals-এর “ভালো” সীমা
প্রথম পাতার JS≤ 150 kB gzipওপরের হিসাব
প্রতি route chunk≤ 100 kB gzipআমাদের নিজের সিদ্ধান্ত
মূল কাজে long task৪ গুণ ধীর CPU-তে ৫০ ms-এর বেশি নয়আমাদের নিজের সিদ্ধান্ত

প্রকাশের আগের তালিকা

আরও গভীরে: Long task-এর সমাধানে ব্রাউজার scheduler.yield() দিচ্ছে, যা কাজ থামিয়ে ব্রাউজারকে সুযোগ দেয়; এর ব্যাপ্তি browser ধরে আলাদা, তাই সমর্থন দেখে নিন। React নিজের scheduler-এ এই কাজটি করে, কিন্তু শুধু transition-এর render-এ, sync render-এ নয়। আর Event Timing API-র duration গোপনতার কারণে ৮ ms-এ গোল করা, তাই সূক্ষ্ম তুলনায় অনেকবার মেপে মধ্যমা নিন।

পরীক্ষা ও ইন্টারভিউতে:

  • memo কেন কাজ করছে না? কোনো prop প্রতি render-এ নতুন reference; inline object বা function.
  • useDeferredValue আর debounce-এর পার্থক্য? প্রথমটির নির্দিষ্ট দেরি নেই, render থামানো যায়; দ্বিতীয়টি নির্দিষ্ট সময় অপেক্ষা করে, request কমায়।
  • Error boundary কী ধরে না? Event handler, async কোড, SSR আর নিজের error.
  • lazy-র শর্ত? Default export, component-এর বাইরে ঘোষণা, ওপরে Suspense.
  • INP-এর ভালো সীমা? ২০০ ms বা কম, ৭৫তম percentile-এ।

১০ · AI-এর যুগে: performance কোড মেপে নিন

AI performance পরামর্শে খুব আত্মবিশ্বাসী, আর কথাগুলো শুনতে ঠিক লাগে। কিন্তু AI আপনার গ্রাহকের ফোন দেখে না। এই পর্বের প্রতিটি শিক্ষা একটি মাপ থেকে এসেছে, আর অন্তত দুটি মাপ অনুমান ভেঙেছে। AI-এর পরামর্শকেও একইভাবে পরীক্ষা করুন।

AI যা প্রায়ই করেসমস্যাযাচাই
সব component-এ memo ছড়ানোঅস্থির props থাকলে লাভ নেই, কোড জটিলআগে-পরে render আর INP মাপুন
memo ছাড়া useDeferredValueকাজ বাড়ে, আমাদের মাপে প্রায় আড়াই গুণRow render গুনুন
সব ছবিতে loading="lazy"LCP ছবি দেরি করেNetwork ট্যাবে ব্যানারের সময় দেখুন
পুরোনো react({ babel }) config@vitejs/plugin-react 6-এ সরানোনিজের version-এর react.dev আর README পড়ুন
Boundary দিয়ে onClick-এর error ধরার দাবিBoundary event handler ধরে নাক্লিকে ইচ্ছা করে throw করে দেখুন
“আবার চেষ্টা” বোতাম দিয়ে chunk error সমাধানআমাদের run-এ retry কাজ করেনিChunk request বন্ধ করে পরীক্ষা করুন
Fallback-এ পুরো error দেখানোভেতরের তথ্য ফাঁসDiff পড়ুন, বার্তা সাধারণ রাখুন
“ছোট করতে” নতুন package যোগBundle উল্টো বাড়ে, অচেনা dependencyVisualizer-এ আগে-পরে তুলনা করুন

AI-কে সমাধান নয়, আগে সন্দেহের তালিকা আর মাপার পরিকল্পনা চান। শেষ শর্তটি আসল।

আমি React 19.3 আর Vite 8 ব্যবহার করি। production build-এ, CPU ৪ গুণ ধীর করে, "তালিকায় যোগ" ক্লিকের মাপ: মোট 240 ms, input delay 6 ms, processing 217 ms, presentation 19 ms. [component-এর কোড] ১. এই ভাগ দেখে সবচেয়ে সম্ভাব্য তিনটি কারণ বলো, কোনটি আগে পরীক্ষা করব। ২. প্রতিটির জন্য Performance প্যানেল বা Profiler-এ কী দেখলে কারণটি নিশ্চিত হবে। ৩. একবারে একটি বদল দেখাও। শুধু যে লাইন বদলাবে সেটি দাও, আর কোন মাপ কতটা বদলানোর কথা বলো। ৪. নতুন package যোগ করো না। প্রতিটি API-র জন্য react.dev-এর পাতা বলো। মাপ না মিললে বদলটি ফিরিয়ে নেব।

AI-এর performance বদল নেওয়ার আগে

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১FID-এর জায়গায় INP এলো কেন? গড় নয়, ৭৫তম percentile কেন?

Google ২০২০ সালে Core Web Vitals ঘোষণা করে। তখন সাড়া দেওয়ার মাপ ছিল FID (First Input Delay). এটি শুধু প্রথম interaction-এর input delay মাপত: handler শুরু হতে কত দেরি। দুটি বড় ফাঁক ছিল। প্রথমত, পরের সব ক্লিক হিসাবের বাইরে। একটি SPA-তে মানুষ কয়েক মিনিট ধরে ক্লিক করেন, আর আসল ধীরতা প্রায়ই পরে আসে। দ্বিতীয়ত, handler-এর নিজের সময় আর ফ্রেম আসার সময় FID দেখত না। আমাদের ক্লিকে input delay ছিল 5.8 ms; FID-এর চোখে এটি নিখুঁত, অথচ ব্যবহারকারী 240 ms অপেক্ষা করেছেন।

INP পুরো পাতার জীবনের ক্লিক, ট্যাপ আর কী চাপ দেখে, আর তিন ভাগই গোনে। ২০২৪ সালের মার্চে এটি Core Web Vital হিসেবে FID-এর জায়গা নেয়।

Percentile কেন? ধরুন ১০০ জনের ৮০ জন ৫০ ms পান, ২০ জন ২,০০০ ms. গড় ৪৪০ ms, যা কেউই পাননি। গড় দুটি ভিন্ন দলকে এক সংখ্যায় মিশিয়ে ফেলে। ৭৫তম percentile বলে, চারজনের তিনজন অন্তত এই মান বা ভালো পাচ্ছেন। এতে আসল ধীর দল চোখে পড়ে, আবার দু-একটি ব্যতিক্রমী সংখ্যা ফলকে নাড়ায় না। পাশাপাশি মোবাইল আর ডেস্কটপ আলাদা দেখা হয়, কারণ দুটি মিলিয়ে দেখলে ফোন লুকিয়ে যায়।

বাংলাদেশের প্রেক্ষাপটে এর অর্থ বড়। আপনার গ্রাহকদের অনেকেই হয়তো কম দামের ফোনে থাকেন। ডেভেলপারের ল্যাপটপে মাপা গড় তাঁদের কথা বলে না। তাই নিজের সাইটের মাঠের ডেটা দরকার।

মূল উপলব্ধি: মেট্রিক বদলানো মানে কী দেখা হচ্ছে তা বদলানো। আপনি যা মাপেন, দল তা-ই ঠিক করে। ভুল মেট্রিক সুন্দর সংখ্যা দিয়ে আসল কষ্ট লুকিয়ে রাখতে পারে।

প্র ০২memo এত ভালো হলে React সব component-এ নিজে থেকে দেয় না কেন? Compiler কি বিতর্ক শেষ করল?

memo-র খরচ আছে। প্রতি render-এ প্রতিটি prop মেলানো, আগের props মেমরিতে রাখা, আর কোডে বাড়তি স্তর। Props প্রায় সবসময় বদলালে মেলানোর কাজটি পুরোপুরি বৃথা। আমাদের “memo + inline function” সারি ঠিক সেই অবস্থা: লেখা হলো, লাভ হলো না।

আসল বাধা অবশ্য অন্য। Props object বা function হলে reference প্রায়ই নতুন, তাই পুরো ব্যবস্থাটি কাজ করতে হলে parent-এর ভেতরেও useMemo আর useCallback লাগে। দীর্ঘদিন সম্প্রদায় এখানে ভাগ ছিল। এক দল বলত, সব জায়গায় memo দিন, কারণ বেছে দেওয়ার চিন্তার চেয়ে মেলানোর খরচ কম। অন্য দল বলত, মাপা ছাড়া নয়, কারণ কোড জটিল হয় আর ভুল dependency-তে বাগ আসে। React-এর docs দ্বিতীয় দিকে ঝুঁকে আছে, তবে স্বীকার করে যে সবখানে দিলেও সাধারণত বড় ক্ষতি নেই, শুধু কোড পড়া কঠিন হয়।

React Compiler এই বিতর্কের চেহারা বদলেছে। ২০২১ সালে React দল “React Forget” নামে এই ধারণা তুলে ধরে, আর ২০২৫ সালের অক্টোবরে version 1.0 আসে। Compiler build-এর সময় কোড বিশ্লেষণ করে, হাতের চেয়ে সূক্ষ্মভাবে জমায়, এমনকি শর্তের পরের মানও। আমাদের run-এ সে হাতে লেখা সেরা সংস্করণের সমান বা ভালো ফল দিয়েছে।

কিন্তু শেষ করেনি। আমাদের run-এই setCount(count + 1) থাকায় ২,০০০ সারি render হয়েছে, কারণ function-টির সত্যিকারের নির্ভরতা বদলেছে। নিয়ম ভাঙা component সে বাদ দেয়। আর পর্ব ১৪-এর run-এ একটি reduce-এর ফলও সে জমায়নি। বড় bundle বা ভারী DOM-এ তার কোনো ভূমিকাই নেই।

মূল উপলব্ধি: টুল সাধারণ ক্ষেত্রের ভার নেয়, চিন্তার ভার নেয় না। “কেন render হলো” বোঝার দক্ষতা এখনো লাগে, শুধু কম বার লাগে।

প্র ০৩Function component-এর যুগে error boundary আজও class কেন? আর boundary event handler ধরে না কেন?

Error boundary এসেছে React 16-এ (২০১৭), যখন React-এর ভেতরটা Fiber-এ নতুন করে লেখা হয়। তার আগে render-এ error হলে React-এর ভেতরের অবস্থা নষ্ট হতে পারত, আর পরের render-এ অদ্ভুত error আসত। React তখন একটি পরিষ্কার নীতি নেয়: ভাঙা UI দেখানোর চেয়ে UI সরিয়ে ফেলা নিরাপদ। ধরুন পেমেন্টের পাতায় ভুল অঙ্ক দেখাচ্ছে; সেটি ফাঁকা পাতার চেয়েও খারাপ।

Class-এর সাথে এই নকশা খাপ খায়, কারণ boundary-র দুটি জিনিস দরকার: render ধাপে চলা একটি pure ফাংশন, আর commit-এর পর চলা একটি side-effect জায়গা। Hook দিয়ে এর সমতুল্য বানানো সম্ভব হতে পারে, কিন্তু একটি জটিলতা আছে। Error ধরার সময় component নিজের children বাদ দিয়ে আবার render হতে হয়। Hook হলে সে কখন, কোন state নিয়ে চলবে, তা নতুন প্রশ্ন তোলে। লেখার সময়ে docs বলে, function সংস্করণ নেই। তাই বাস্তব পথ একটি ছোট class, অথবা react-error-boundary-এর মতো library, যা ভেতরে class-ই ব্যবহার করে।

Event handler কেন ধরে না? Boundary মূলত render চলাকালীন tree-র error ধরে। Handler render-এর বাইরে চলে। সেখানে error হলে পর্দার UI অক্ষত, তাই React-এর পুরো পাতা সরানোর কোনো কারণ নেই। একটি ক্লিক ব্যর্থ হলে সাধারণ try/catch দিয়ে “যোগ করা গেল না” দেখানোই সঠিক অভিজ্ঞতা। একইভাবে setTimeout এমন সময় চলে, যখন React-এর কাছে কোনো tree-র প্রসঙ্গ নেই।

কিছু সেতু আছে। react-error-boundary-এর useErrorBoundary ধরা error-কে নিকটতম boundary-তে পাঠায়। React 19-এর transition function-এর error-ও boundary পায়।

মূল উপলব্ধি: Error handling-এর সীমা ঠিক হয় “কী ক্ষতি হয়েছে” দিয়ে। Render ভাঙলে UI-এর সত্য হারায়, তাই boundary লাগে। Handler ভাঙলে শুধু একটি কাজ ব্যর্থ, তাই স্থানীয়ভাবে সামলান।

প্র ০৪যত ভাগ, তত ভালো? Code splitting কোথায় উল্টো ফল দেয়, আর chunk কতটি হওয়া উচিত?

Code splitting-এর ইতিহাস bundler-এর সাথে জড়ানো। JavaScript-এ বহুদিন module ছিল না। Browserify আর webpack সব ফাইল জুড়ে একটি bundle বানাত। বাড়তে বাড়তে bundle বিশাল হলো, আর webpack ধাপে ধাপে ভাগ করার পথ দিল। ভাষায় import() মান হিসেবে আসে ES2020-তে, আর React-এর lazy ও Suspense আসে ২০১৮ সালে, React 16.6-এ।

ভাগ বাড়ালে তিনটি উল্টো ফল হতে পারে। প্রথমত, পরপর নির্ভরতা। আমাদের run-এ হোম lazy করায় মূল chunk চলার পরে আরেকটি request গেল, প্রায় 380 ms দেরি। ১৫০ ms RTT-তে প্রতিটি অতিরিক্ত ধাপ দামি। দ্বিতীয়ত, সংকোচনের ক্ষতি। gzip একই ধরনের কোডের মধ্যে মিল খোঁজে, তাই অনেক ছোট ফাইলের যোগফল এক ফাইলের চেয়ে বড় হয়। Vite build-এ দেখুন, 665 kB-র একটি ফাইল gzip-এ 205 kB, আর lazy ভাগগুলোর যোগফল 79.62 + 3.38 + 26.33 + 98.07 + ছোট দুটি ≈ 207.8 kB. বেশি নয়, কিন্তু কাজটি বিনামূল্যে নয়। তৃতীয়ত, পরের পাতায় অপেক্ষা: 160 ms থেকে 840 ms.

আবার একটি বড় সুবিধা প্রায়ই ভুলে যাওয়া হয়। Content hash-এর কারণে ছোট chunk আলাদাভাবে cache হয়। একটি পাতা বদলালে সেই chunk-ই নতুন, React-এর chunk ব্রাউজারে থেকে যায়। HTTP/2 অনেক ফাইল এক সংযোগে আনতে পারে, তবু খরচ শূন্য হয় না।

তাই সংখ্যা ঠিক করার সূত্র নেই। ব্যবহারযোগ্য নিয়ম: route ধরে ভাগ করুন, প্রথম পাতা সরাসরি রাখুন, ভারী ও কম-ব্যবহৃত অংশ (চার্ট, editor, অ্যাডমিন) আলাদা করুন, ছোট component আলাদা করবেন না।

মূল উপলব্ধি: Code splitting খরচ কমায় না, সরিয়ে দেয়। প্রশ্ন হলো কার খরচ কোথায় পড়লে ব্যবহারকারীর মোট অভিজ্ঞতা ভালো হয়।

প্র ০৫Performance-এর কাজ কখন থামাবেন? Budget কি শুধু একটি সংখ্যা, না একটি দলীয় চুক্তি?

Donald Knuth-এর ১৯৭৪ সালের বহুল-উদ্ধৃত কথা আছে: premature optimization সব সমস্যার মূল। প্রায়ই ভুলে যাওয়া হয়, একই লেখায় তিনি গুরুত্বপূর্ণ ৩% কোডে সুযোগ না ছাড়ার কথাও বলেছিলেন। মানে মাপ দিয়ে সেই অংশ খুঁজুন, বাকিটা ছেড়ে দিন।

Performance কাজের শেষ নেই। তাই থামার নিয়ম আগেই ঠিক করতে হয়। সেটিই budget. “INP ≤ ২০০ ms, প্রথম পাতার JS ≤ 150 kB” লেখা থাকলে দুটি ভালো জিনিস হয়। সীমার ভেতরে থাকলে আপনি থামতে পারেন, ফিচারের কাজে ফিরতে পারেন। সীমা পার হলে “দ্রুত না ফিচার” নিয়ে তর্ক সংখ্যায় নামে: এই নতুন চার্ট লাইব্রেরি 98 kB আনছে, তাহলে কী বাদ যাবে, নাকি সেটি lazy হবে?

Budget ছাড়া কী হয়? Performance ধীরে ধীরে ক্ষয়ে যায়। কোনো একটি commit খারাপ নয়, প্রতিটি মাত্র কয়েক kB. এক বছর পরে অ্যাপ দ্বিগুণ ভারী, কেউ দায়ী নয়। তাই অনেক দল CI-তে bundle-এর আকার পরীক্ষা করে, সীমা ছাড়ালে build ব্যর্থ করে। Lighthouse CI বা size-limit-এর মতো টুল এর উদাহরণ; পাইপলাইনের ধারণা আছে Cloud & DevOps কোর্সে, আর পর্ব ৮৪-এ বিস্তারিত।

কিন্তু budget ব্যবসার সিদ্ধান্তও। একটি অভ্যন্তরীণ অ্যাডমিন প্যানেল, যা অফিসের কম্পিউটারে চলে, সেখানে 100 kB-র চিন্তা অর্থহীন। গ্রামের কোনো গ্রাহকের সস্তা ফোনে চলা দোকানের হোম পাতায় একই 100 kB হয়তো ক্রয় আর ফিরে যাওয়ার পার্থক্য। Budget ঠিক করে আপনার ব্যবহারকারী, ফ্যাশন নয়।

মূল উপলব্ধি: ভালো ইঞ্জিনিয়ার সব জায়গায় দ্রুততম কোড লেখেন না। তিনি জানেন কোথায় দ্রুত হওয়া দরকার, কতটা দরকার, আর কখন থামতে হয়।

অনুশীলন

হিসাব, অনুমান আর নকশা, তিন রকম কাজ। আগে নিজে চেষ্টা করুন।

  1. বাজেটের হিসাব: ধরুন নেটওয়ার্ক 125 kB/s, RTT 200 ms, লক্ষ্য ৪ সেকেন্ড। সংযোগ আর HTML-এ ৪ RTT. চালানোর খরচ 3 ms/kB. সব সম্পদের বাজেট কত? অর্ধেক JavaScript হলে কত? 205 kB-র bundle কি এতে চলবে?

    সংযোগ: ৪ × 200 = 800 ms. বাকি 4000 − 800 = 3200 ms. নামানো: 1 ÷ 125 = 8 ms/kB. মোট 8 + 3 = 11 ms/kB. বাজেট 3200 ÷ 11 ≈ 290 kB. অর্ধেক JavaScript ≈ 145 kB. 205 kB এই সীমার বাইরে, তাই ভাগ করতে হবে। সব সম্পদ ধরলে 205 kB একা বাজেটের প্রায় ৭১%, ছবি আর CSS-এর জন্য জায়গা প্রায় থাকে না।

  2. আউটপুট অনুমান: Child হলো memo করা, আর console.log('child') লেখে। App-এ const [n, setN] = useState(0); আর <Child items={[1, 2]} />. একটি বোতাম setN(n + 1) ডাকে। Production build, compiler ছাড়া। পাতা খোলার পর দুবার বোতাম চাপলে মোট কতবার child ছাপা হবে? const items = [1, 2]; বাইরে সরালে?

    প্রথম ক্ষেত্রে ৩ বার: mount-এ ১, প্রতিটি ক্লিকে ১. [1, 2] প্রতি render-এ নতুন array, তাই memo হারে। বাইরে সরালে ১ বার: reference একই, তাই শুধু mount. React Compiler থাকলে প্রথম ক্ষেত্রেও সাধারণত array-টি জমা হতো।

  3. নিজের bundle দেখুন: নিজের যেকোনো Vite প্রজেক্টে rollup-plugin-visualizer যোগ করে build দিন, stats.html খুলুন। সবচেয়ে বড় তিনটি package লিখুন। প্রতিটির জন্য বলুন: সব পাতায় দরকার, এক পাতায় দরকার, না একেবারেই দরকার নেই?

    সাধারণত react-dom সবচেয়ে বড়, এবং সব পাতায় লাগে; একে কিছু করার নেই। চার্ট, editor, date library প্রায়ই “এক পাতায়” দলে পড়ে, তাই lazy. পুরো lodash, মুছে ফেলা ফিচারের package, বা দুবার ঢোকা একই package “দরকার নেই” দলে। প্রতিবার বদলে আবার build দিয়ে সংখ্যা মেলান। Package সরালে test চালাতে ভুলবেন না।

  4. নকশা সমালোচনা: একটি দলের পণ্যের পাতা: পুরো অ্যাপের মাথায় একটিমাত্র error boundary, সব route-এ একটিমাত্র Suspense, আর প্রতিটি ছোট কার্ডে আলাদা Suspense. রিভিউ API ভাঙলে কী হবে? আপনি কীভাবে সাজাবেন?

    রিভিউ ভাঙলে একমাত্র boundary পুরো অ্যাপ ঢেকে দেয়: মেনু আর কেনার বোতাম সব যায়। আর কার্ড-প্রতি Suspense পাতায় একের পর এক ঝলক আনে। সাজানো: মেনু boundary-র বাইরে; প্রতি route-এ একটি error boundary আর একটি Suspense; রিভিউর মতো ঐচ্ছিক ধীর অংশে আলাদা জোড়া। কেনার বোতামের অংশ কোনো ঐচ্ছিক অংশের ওপর নির্ভর করবে না। তালিকার কার্ডগুলো এক boundary-তে, একসাথে আসুক। Log পাঠাতে ভুলবেন না; নীরবে ধরা error মানে অজানা বাগ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি ধীর ক্লিক ঠিক করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. একটি নির্দিষ্ট ধীর কাজ বেছে নেওয়া
  2. Production build-এ, CPU ধীর করে মাপা
  3. Input, processing আর presentation-এর ভাগ দেখা
  4. Profiler-এ সবচেয়ে ভারী component খোঁজা
  5. একটি কারণ ধরে একটি বদল করা
  6. একই শর্তে আবার মাপা

একটি React component-কে memo দিয়ে মোড়া হয়েছে, অথচ parent render হলেই সে চলছে। Prop হিসেবে যাচ্ছে onAdd={() => add(id)}. সবচেয়ে সম্ভাব্য কারণ কী?

  • memo শুধু development build-এ কাজ করে
  • memo function component-এ চলে না
  • Inline function প্রতি render-এ নতুন, তাই prop “বদলেছে” ধরা হয়
  • memo function-এর ভেতরের কোড গভীরভাবে মেলাতে পারে না বলে থেমে যায়

একটি ক্লিকের মাপ: input delay 6 ms, processing 220 ms, presentation 20 ms. কোন দিকে আগে খোঁজা উচিত?

  • ক্লিকের আগে চলা long task
  • Handler আর render-এর কাজ
  • বিশাল DOM আর layout
  • ধীর নেটওয়ার্ক

React-এ useDeferredValue দিয়ে ভারী তালিকার filter করা হলো, কিন্তু তালিকার component memo করা নেই। কী ঘটবে?

  • React error দেবে
  • টাইপিং পুরোপুরি মসৃণ হবে
  • তালিকা নির্দিষ্ট 300 ms পরে render হবে
  • তালিকা জরুরি render-এও চলবে, ফলে লাভ থাকবে না

React-এর একটি ক্লাস error boundary নিচের কোন error সরাসরি ধরে?

  • Child component render করার সময় throw করা error
  • Button-এর onClick-এর ভেতরের error
  • setTimeout callback-এর error
  • Boundary-র নিজের render-এর error

ধরুন নেটওয়ার্ক 200 kB/s আর চালানোর খরচ 3 ms/kB. অন্য খরচ বাদে 2,400 ms সময় হাতে থাকলে সর্বোচ্চ কত kB সম্পদ পাঠানো যায়?

  • 480 kB
  • 800 kB
  • 300 kB
  • 150 kB

React অ্যাপের হোম পাতার ওপরের বড় ব্যানার ছবিটি LCP element. কোন attribute-এর জোড়া ভালো?

  • loading="lazy" আর fetchpriority="high"
  • fetchpriority="high", সাথে width আর height
  • fetchpriority="low" আর loading="lazy"
  • শুধু decoding="async", কোনো মাপ ছাড়া

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. আগে মাপুন: Production build, ধীর CPU, একটি নির্দিষ্ট কাজ। INP-এর তিন ভাগ দেখে পথ ঠিক করুন।
  2. কারণ আগে সরান, তারপর memo. স্থির props ছাড়া memo অকেজো; compiler অনেকটা করে, সব নয়।
  3. ভারী filter-এ useDeferredValue + memo; কাজ কমে না, টাইপিং সচল থাকে।
  4. ভারী route lazy, প্রথম পাতা নয়। Suspense অপেক্ষা দেখায়, navigation-এ key দিয়ে fallback আনুন।
  5. Route-প্রতি error boundary, মেনু বাইরে। Chunk ব্যর্থ হলে reload-এর পথ রাখুন, আর সংখ্যায় লেখা budget ধরে কাজ করুন।

পরের পর্বে: মডিউল ৩ শেষ। React-এর সামনের দিক এখন আপনার হাতে। এবার সার্ভারের দিকে: Node.js-এর http module, event loop, আর একটি সার্ভার একসাথে অনেক request কীভাবে সামলায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান