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

পারফরম্যান্স, memory leak আর DevTools — মেপে দ্রুত করা

JavaScript Performance, Memory Leaks & DevTools — Core Web Vitals, Bundles, Long Tasks, Heap Snapshots

Taught in Bangla, this lesson teaches measuring before optimising: Core Web Vitals like LCP, INP and CLS, the network cost of JavaScript bundles, keeping the main thread responsive, algorithms before micro-optimisation, what the garbage collector can and cannot free, finding memory leaks with heap snapshots, DevTools Performance and Lighthouse, Node profiling and performance budgets in CI.

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

  • Core Web Vitals — LCP, INP, CLS আর সহায়ক TTFB, FCP — কী মাপে, সীমা কত, lab আর field ডেটার পার্থক্য
  • নেটওয়ার্কের খরচ কমানো: bundle size, code splitting, compression, cache, ছবি, font আর render-blocking resource
  • Runtime-এ পাতা সচল রাখা: long task ভাঙা, layout thrashing, লম্বা তালিকা, debounce/throttle, Worker — আর অ্যালগরিদম বদলের শক্তি
  • Garbage collection কীভাবে কাজ করে, সাধারণ memory leak কোথা থেকে আসে, আর কীভাবে ঠেকাবেন
  • DevTools-এর Performance, Memory, Lighthouse, Network ও Coverage প্যানেল, Node.js profiling আর CI-তে performance budget

১ · পারফরম্যান্স মানে ব্যবহারকারীর অভিজ্ঞতা — আর আগে মাপা

“সাইটটা ধীর” — এর ভেতরে আসলে চারটি আলাদা অভিযোগ লুকিয়ে থাকে। (১) দেখতে দেরি: লিংকে চাপার পর অনেকক্ষণ সাদা পাতা, বা পণ্যের ছবি আসতেই চায় না। (২) সাড়া দিতে দেরি: “কার্টে যোগ করুন” চাপলাম, কিছুই হলো না, আবার চাপলাম — দুটো যোগ হয়ে গেল। (৩) পাতা লাফানো: পড়ছিলাম, হঠাৎ একটি ব্যানার এসে লেখা নিচে ঠেলে দিল, আর ভুল বোতামে চাপ পড়ল। (৪) সময়ের সাথে ধীর হওয়া: শুরুতে ঠিক ছিল, আধা ঘণ্টা ব্যবহারের পর ফোন গরম, পাতা থেমে থেমে চলে। প্রতিটির কারণ আলাদা, মাপার উপায় আলাদা, সমাধানও আলাদা।

একজন ভালো ডাক্তার রোগী “শরীর খারাপ” বললেই ওষুধ লেখেন না। আগে জ্বর মাপেন, রক্তচাপ দেখেন, দরকারে পরীক্ষা করান। রোগ ধরা পড়লে শুধু সেটার চিকিৎসা দেন, তারপর আবার মেপে দেখেন কাজ হলো কি না। অনুমানে পাঁচ রকম ওষুধ দিলে কোনটা কাজ করল কেউ জানে না, বাকিগুলোর পার্শ্বপ্রতিক্রিয়া থেকে যায়। পারফরম্যান্সের কাজও ঠিক এমন: থার্মোমিটার = মেট্রিক, পরীক্ষাগার = DevTools, রোগ = আসল বাধা (bottleneck).

তাই এই পর্বের মূল মন্ত্র একটি চক্র: মাপুন → খুঁজুন → ঠিক করুন → আবার মাপুন। মাপার দুটি উৎস। এক, আসল ব্যবহারকারীদের যন্ত্র থেকে আসা সংখ্যা — যাকে বলে field data. দুই, নিজের কম্পিউটারে DevTools দিয়ে নিয়ন্ত্রিত পরীক্ষা — lab data. Field data বলে কোথায় সমস্যা; lab-এ profiling করে বোঝা যায় কেন।

কেন অনুমানে কাজ হয় না? কারণ প্রোগ্রামের সময় সমানভাবে ছড়ানো থাকে না। প্রায় সবসময় অল্প কয়েকটি জায়গায় বেশিরভাগ সময় যায়। ধরুন একটি ক্লিকে ৫০০ ms লাগে, যার ৪৫০ ms যায় ২,০০০টি কার্ড আঁকায়, আর ১০ ms একটি লুপে। আপনি লুপটিকে দ্বিগুণ দ্রুত করলেন — বাঁচল ৫ ms, ১%. অথচ কার্ড আঁকা অর্ধেক করলে বাঁচে ২২৫ ms. কোন অংশ বড়, তা শুধু মেপেই জানা যায় (ভাবনার প্রশ্ন ৫-এ Amdahl-এর যুক্তি দেখুন)।

মূল ধারণা

পারফরম্যান্সের লক্ষ্য “কোড দ্রুত” নয় — লক্ষ্য “ব্যবহারকারীর কাছে দ্রুত”। আর সেটার একমাত্র নির্ভরযোগ্য পথ: আসল ব্যবহারকারীর মেট্রিক দিয়ে সমস্যা খোঁজা, profiler দিয়ে কারণ খোঁজা, সবচেয়ে বড় কারণটা ঠিক করা, আর আবার মেপে প্রমাণ করা।

২ · Core Web Vitals — ব্যবহারকারীর অনুভূতিকে সংখ্যায় মাপা

“দ্রুত লাগছে” কথাটিকে সংখ্যায় আনতে Google ২০২০ সালে Core Web Vitals নামে তিনটি মেট্রিক ঘোষণা করে। প্রতিটি উপরের একটি অভিযোগের সাথে মেলে। সাথে দুটি সহায়ক মেট্রিক আছে, যা সমস্যার উৎস খুঁজতে সাহায্য করে:

মেট্রিককী মাপেভালোখারাপ
LCP (Largest Contentful Paint)পাতার সবচেয়ে বড় দৃশ্যমান লেখা বা ছবি কখন আঁকা হলো — “মূল জিনিস দেখা গেল”≤ ২.৫ সেকেন্ড> ৪ সেকেন্ড
INP (Interaction to Next Paint)ক্লিক, ট্যাপ বা কী-চাপ থেকে পরের বার পর্দা আঁকা পর্যন্ত সময় — পুরো ভিজিটের প্রায় সবচেয়ে ধীরটি≤ ২০০ ms> ৫০০ ms
CLS (Cumulative Layout Shift)অপ্রত্যাশিতভাবে জিনিস কতটা সরে গেল — একটি একক ছাড়া স্কোর≤ ০.১> ০.২৫
TTFB (সহায়ক)request পাঠানো থেকে সার্ভারের উত্তরের প্রথম byte আসা পর্যন্ত≤ ০.৮ সেকেন্ড> ১.৮ সেকেন্ড
FCP (সহায়ক)প্রথম যেকোনো লেখা বা ছবি আঁকা — “সাদা পাতা শেষ”≤ ১.৮ সেকেন্ড> ৩ সেকেন্ড

দুটি খুঁটিনাটি মনে রাখুন। প্রথমত, এই সীমা গড়ের ওপর নয় — বিচার হয় ৭৫তম percentile-এ। অর্থাৎ ভিজিটগুলোর অন্তত ৭৫% সীমার মধ্যে থাকলে তবেই “ভালো” — গড়ে ধীর ফোনের ব্যবহারকারীরা হারিয়ে যান, percentile-এ যান না। মোবাইল আর ডেস্কটপের হিসাব আলাদা। দ্বিতীয়ত, INP এসেছে ২০২৪ সালের মার্চে, আগের মেট্রিক FID-এর জায়গায়। পর্ব ৩১-এ দেখেছেন, INP-এর তিন ভাগ: input delay, processing আর presentation delay.

CLS-এর দুটি চেনা অপরাধী

একটি খবরের সাইটের কথা ভাবুন। লেখার মাঝে একটি ছবি আছে, কিন্তু <img>-এ width আর height দেওয়া নেই। ব্রাউজার প্রথমে জানে না ছবিটি কত বড়, তাই শূন্য জায়গা রেখে লেখা সাজায়। ছবি নামার পর হঠাৎ ৩০০ pixel জায়গা লাগে — নিচের সব লেখা ঝাঁকি খেয়ে সরে যায়। দ্বিতীয় অপরাধী দেরিতে আসা ব্যানার: পাতা দেখানোর এক সেকেন্ড পর JavaScript ওপরে “অ্যাপ নামান” ব্যানার ঢোকায়, আর পুরো পাতা নিচে নামে। সমাধান দুটোই সরল:

<!-- ছবির মাপ আগেই জানান — ব্রাউজার ঠিক জায়গা রেখে দেয় -->
<img src="padma-bridge.webp" width="800" height="450" alt="পদ্মা সেতু">

<!-- দেরিতে আসা ব্যানারের জন্য আগে থেকেই জায়গা সংরক্ষণ -->
<div class="promo-slot" style="min-height: 64px"></div>

width আর height থেকে ব্রাউজার ছবির অনুপাত (aspect ratio) হিসাব করে, তাই CSS-এ width: 100% দিলেও উচ্চতা ঠিক থাকে। ব্যানার, বিজ্ঞাপন বা embed-এর জন্য আগেই নির্দিষ্ট উচ্চতার জায়গা রাখুন, অথবা সেগুলো পাতার ওপরে না ঢুকিয়ে নিচে বা overlay হিসেবে দেখান। ব্যবহারকারীর নিজের ক্লিকের ৫০০ ms-এর মধ্যে ঘটা সরে যাওয়া CLS-এ গোনা হয় না — যেমন “আরও দেখুন” চাপলে তালিকা বড় হওয়া।

Lab বনাম field — দুই রকম সত্য

Lighthouse (DevTools-এর ভেতরে, বা PageSpeed Insights-এ) একটি নিয়ন্ত্রিত পরিবেশে পাতা একবার লোড করে স্কোর দেয় — এটি lab data. যখন খুশি চালানো যায়, কিন্তু যন্ত্র আর নেটওয়ার্ক নকল, আর কোনো আসল ক্লিক নেই — তাই lab-এ INP মাপা যায় না, Lighthouse তার বদলে Total Blocking Time দেখায়। অন্যদিকে field data আসে আসল মানুষের আসল ফোন থেকে। Chrome ব্যবহারকারীদের (যাঁরা অনুমতি দিয়েছেন) সমষ্টিগত তথ্য Google প্রকাশ করে CrUX (Chrome User Experience Report) নামে, যা PageSpeed Insights আর Search Console-এ দেখা যায়। নিজের সাইটে নিজেই মাপতে চাইলে RUM (Real User Monitoring) — সবচেয়ে সহজ উপায় Google-এর ছোট web-vitals library:

import { onLCP, onINP, onCLS } from "web-vitals";

function send(metric) {
  // যেমন { name: "INP", value: 312, rating: "needs-improvement" }
  navigator.sendBeacon("/analytics", JSON.stringify(metric));
}
onLCP(send);
onINP(send);
onCLS(send);

এই কোড npm package লাগে বলে এখানে চালানো যাবে না (npm আর import নিয়ে পর্ব ৪৫ ও ৪৬)। প্রতিটি onX মেট্রিক চূড়ান্ত হলে callback ডাকে — INP আর CLS চূড়ান্ত হয় পাতা লুকানো বা বন্ধ হওয়ার সময়, তাই সাধারণ fetch-এর বদলে sendBeacon, যা পাতা বন্ধ হলেও ডেটা পাঠিয়ে দেয়। ভেতরে এটি ব্রাউজারের PerformanceObserver ব্যবহার করে (পর্ব ৪১)।

সাবধান: Lighthouse-এ ১০০ পাওয়া মানেই ব্যবহারকারী খুশি নন। আপনার ল্যাপটপ আর অফিসের ব্রডব্যান্ড বাংলাদেশের বেশিরভাগ ব্যবহারকারীর বাস্তবতা নয় — অনেকে কম দামের Android ফোনে, মোবাইল ডেটায়, ভিড়ের সময়ে দুর্বল নেটওয়ার্কে পাতা খোলেন। Lab-এ সমস্যা খুঁজুন, কিন্তু সাফল্য মাপুন field data দিয়ে।

৩ · নেটওয়ার্কের খরচ — JavaScript সবচেয়ে দামি byte

১০০ KB-র একটি JPEG ছবি আর ১০০ KB-র JavaScript — নামানোর খরচ সমান, তারপরের খরচ নয়। ছবি decode করে আঁকলেই শেষ, প্রায়ই main thread-এর বাইরে। JavaScript-কে নামানোর পর parse করতে হয়, compile করতে হয়, তারপর চালাতে হয় — আর এর বড় অংশ main thread-এ (পর্ব ০৫-এর engine pipeline মনে করুন)। কম দামের ফোনে এই কাজ দামি ফোনের কয়েক গুণ সময় নিতে পারে। তাই bundle size — পাতা যত KB JavaScript পাঠায় — পারফরম্যান্সের সবচেয়ে বড় নির্ধারকগুলোর একটি।

✂️ Code splitting

সব কোড এক ফাইলে না পাঠিয়ে টুকরো করুন। প্রথম পাতার জন্য যা লাগে শুধু সেটুকু শুরুতে; বাকিটা দরকারের সময় import() দিয়ে আনুন (পর্ব ৪৫)। Bundler প্রতিটি import()-কে আলাদা ফাইল বানায় — একে বলে code splitting.

শুধু যা এখন লাগে

🌳 Tree-shaking আর minify

ES module-এর static import দেখে bundler বোঝে কোন function কোথাও ব্যবহার হয় না, আর সেগুলো ফেলে দেয় — tree-shaking (পর্ব ৪৬)। Minification নাম ছোট করে, ফাঁকা জায়গা মুছে আকার আরও কমায়।

যা লাগে না, তা পাঠাবেন না

🗜️ Compression

সার্ভার ফাইল gzip বা Brotli দিয়ে চেপে পাঠায়, ব্রাউজার খুলে নেয়। লেখা-ভিত্তিক ফাইল (JS, CSS, HTML, JSON) প্রায়ই কয়েক ভাগের এক ভাগ হয়ে যায়। মনে রাখুন: compression নামানো কমায়, কিন্তু parse আর compile হয় পুরো খোলা আকারের ওপর।

তারে কম, ফোনে একই

🗄️ Cache আর hash-যুক্ত নাম

Build টুল ফাইলের নামে বিষয়বস্তুর hash বসায় — app.3f9a1c.js — যাকে বলে content hashing. বিষয়বস্তু বদলালে নামও বদলায়, তাই এই ফাইলকে এক বছরের Cache-Control দেওয়া নিরাপদ; HTML-কে নয়। Service worker দিয়ে cache-এর কথা পর্ব ৪৪-এ।

একবার নামান, বারবার ব্যবহার

বাস্তব ঘটনা: ২ MB JavaScript-এর পণ্য-তালিকা

একটি অনলাইন দোকানের পণ্য-তালিকার পাতা মোবাইলে খুলতে অনেক সময় নিচ্ছিল; field data-য় LCP ছিল “খারাপ” সীমার অনেক ওপরে। DevTools-এর Network প্যানেলে দেখা গেল, পাতা মোট প্রায় ২ MB (খোলা আকারে) JavaScript নামাচ্ছে। Bundle বিশ্লেষণে তিনটি বড় অংশ: একটি চার্ট library (শুধু “দামের ইতিহাস” ট্যাবে লাগে, যা খুব কম লোক খোলেন), পুরো একটি তারিখ-library (দুটি function ব্যবহারের জন্য), আর সব পাতার কোড একসাথে। সমাধান তিন ধাপে: চার্ট দেরিতে আনা, তারিখ-library-র বদলে ব্রাউজারের নিজের Intl (পর্ব ২০), আর বাইরের library-গুলো আলাদা vendor chunk-এ রাখা, যাতে অ্যাপের কোড বদলালেও ব্যবহারকারীর cache-এ থাকা library আবার নামাতে না হয়।

// আগে: import { renderChart } from "./price-chart.js";  ← সবার জন্য, শুরুতেই
historyTab.addEventListener("click", async () => {
  historyTab.disabled = true;
  const { renderChart } = await import("./price-chart.js");  // প্রথম ক্লিকে নামে
  renderChart(chartBox, priceHistory);
}, { once: true });

এখন চার্টের কোড শুধু তাঁরাই নামান, যাঁরা ট্যাবটি খোলেন। import() একটি Promise দেয়, তাই await; আর bundler এই লাইন দেখে price-chart.js-কে আলাদা ফাইলে রাখে। দেরি কমাতে মাউস ট্যাবের ওপর এলেই (hover) import() শুরু করে রাখা যায়। আসল কথা: মেপে বড় অংশগুলো খুঁজে বের করা।

ছবি আর font

বেশিরভাগ পাতায় LCP element একটি ছবি। তিনটি নিয়ম বড় পার্থক্য আনে। (১) আধুনিক ফরম্যাট — WebP আর AVIF সাধারণত একই মানের JPEG বা PNG-এর চেয়ে বেশ ছোট। (২) পর্দার মাপ অনুযায়ী ছবি — srcset আর sizes দিলে ফোন ছোট ছবিটি নেয়, ৪K মনিটর বড়টি। (৩) পর্দার বাইরের ছবিতে loading="lazy" (lazy loading) — কিন্তু LCP ছবিতে কখনো নয়; সেটিকে বরং fetchpriority="high" দিয়ে আগে আনুন।

<!-- ওপরের বড় ছবি (LCP): আগে আনুন, lazy নয় -->
<img src="hero-800.avif" srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1600.avif 1600w"
     sizes="100vw" width="1600" height="900" fetchpriority="high" alt="ঈদের অফার">

<!-- নিচের পণ্যের ছবি: দেখার কাছাকাছি এলে নামবে -->
<img src="shirt.webp" width="300" height="300" loading="lazy" alt="সুতির শার্ট">

Web font নামার আগে ব্রাউজার লেখা লুকিয়ে রাখতে পারে — বাংলা font প্রায়ই বড় ফাইল, তাই এটা আরও চোখে পড়ে। CSS-এর @font-face-এ font-display: swap দিলে আগে system font-এ লেখা দেখা যায়, font নামলে বদলে যায়; শুধু দরকারি weight আর অক্ষর-সীমা (subset) রাখলে ফাইলও ছোট হয়।

Critical rendering path আর render-blocking resource

ব্রাউজার HTML থেকে DOM, CSS থেকে CSSOM বানায়, দুটো মিলিয়ে render tree, তারপর layout আর paint — এই ধারাকে বলে critical rendering path. <head>-এর সাধারণ <link rel="stylesheet"> আর defer/async ছাড়া <script> এই ধারা থামিয়ে দেয় — এরা render-blocking. পর্ব ৩৭-এ দেখা নিয়মটাই এখানে পারফরম্যান্সের হাতিয়ার: নিজের script-এ defer (বা type="module", যা নিজেই defer), স্বাধীন বাইরের script-এ (যেমন analytics) async, আর শুধু প্রথম পর্দার জন্য দরকারি অল্প CSS.

৪ · Runtime — ট্যাপে সাড়া দেওয়া পাতা

পাতা লোড হয়ে গেলেই কাজ শেষ নয়। এরপর আসে INP-এর প্রশ্ন: ব্যবহারকারী চাপলে পাতা কত দ্রুত সাড়া দেয়? পর্ব ৩১-এ দেখেছেন, main thread একসাথে একটিই কাজ করে, আর ৫০ ms-এর বেশি একটানা কাজ হলো long task. Runtime-এর সমস্যার প্রায় সবই শেষ পর্যন্ত এক জায়গায় দাঁড়ায় — main thread খুব বেশি সময় ব্যস্ত। চলুন একটি আসল ধাঁচের তদন্ত ধাপে ধাপে দেখি: একটি পণ্য-তালিকায় “দাম অনুযায়ী সাজান” বোতাম ফোনে ধীর।

“দাম অনুযায়ী সাজান” বোতামের তদন্ত (সংখ্যাগুলো উদাহরণ)
ধাপ
টুল
যা দেখা গেল
সিদ্ধান্ত
  1. মাপুন — field থেকে। RUM-এর ড্যাশবোর্ডে মোবাইলের INP ৭৫তম percentile-এ ৬৫০ ms. সবচেয়ে ধীর interaction: “দাম অনুযায়ী সাজান” বোতাম। এখন জানি কোথায় খুঁজতে হবে।
  2. Lab-এ ঘটান। DevTools-এর Performance প্যানেলে CPU throttling ৪× দিয়ে (কম দামের ফোনের মতো) record চাপলাম, বোতামে ক্লিক করলাম, থামালাম। Main সারিতে একটি ৪৮০ ms-এর task — ওপরের কোণে লাল ত্রিভুজ আর লাল দাগ মানে long task.
  3. খুঁজুন — flame chart পড়ে। সবাই ভেবেছিল sort ধীর — অথচ sort মাত্র ২০ ms. ৩৯০ ms যাচ্ছে renderCards-এ, যেখানে লুপের ভেতরে বারবার বেগুনি “Layout” ব্লক — প্রতিটি কার্ড বসিয়ে offsetHeight পড়া হচ্ছে। এটাই layout thrashing.
  4. শুধু আসল বাধা ঠিক করুন। সব ২,০০০ কার্ড না এঁকে শুধু পর্দায় দৃশ্যমান ৩০টির মতো আঁকা হলো (virtual list), আর সব DOM-পড়া আগে, সব লেখা পরে। Sort-এর কোডে হাতই দেওয়া হয়নি — সেটা কখনো সমস্যা ছিল না।
  5. আবার মাপুন। একই throttling-এ ক্লিকের task এখন প্রায় ৬০ ms. Deploy-এর পর কয়েক সপ্তাহের field data-য় INP ২০০ ms-এর নিচে — সংখ্যা দিয়ে প্রমাণ।

Runtime-এর পাঁচটি পরিচিত সমাধান

  • Long task ভাঙুন: বড় লুপ টুকরো করে মাঝে main thread ছেড়ে দিন — scheduler.yield() বা setTimeout দিয়ে (পর্ব ৩১-এর chunking). আগে পর্দায় সাড়া দেখান (“সাজানো হচ্ছে…”), তারপর ভারী কাজ।
  • Layout thrashing এড়ান: লুপে DOM-এ লেখা আর offsetHeight, getBoundingClientRect()-এর মতো মাপ পড়া মিশিয়ে ফেললে ব্রাউজার প্রতিবার নতুন করে layout হিসাব করে — layout thrashing (পর্ব ৩৭)। সব পড়া একসাথে, তারপর সব লেখা।
  • লম্বা তালিকা virtualize করুন: ১০,০০০ সারির তালিকায় ১০,০০০ DOM node রাখবেন না। শুধু দৃশ্যমান সারিগুলো আঁকুন, scroll করলে সেগুলোতেই নতুন ডেটা বসান — list virtualization. সহজ বিকল্প হিসেবে CSS-এর content-visibility: auto পর্দার বাইরের অংশের rendering পিছিয়ে দেয়।
  • ঘন ঘন ঘটা event সামলান: প্রতিটি অক্ষরে খোঁজার request নয় — debounce; scroll-এ প্রতি pixel-এ হিসাব নয় — throttle (পর্ব ৩৬)।
  • সত্যিকারের ভারী হিসাব সরিয়ে দিন: CSV parse, ছবি প্রসেসিং, বড় রিপোর্ট — Web Worker-এ (পর্ব ৪২)। Worker দ্রুত করে না, main thread খালি রাখে।

৫ · অ্যালগরিদম আগে, micro-optimization পরে

একটি অনলাইন দোকানের অ্যাডমিন পাতায় “পেমেন্ট মিলানো” ফিচার। এক তালিকায় আজকের অর্ডারের id, আরেক তালিকায় পেমেন্ট গেটওয়ে থেকে আসা পরিশোধিত id. কোন অর্ডারের টাকা এসেছে? প্রথম যে কোডটা মাথায় আসে (আর AI-ও প্রায়ই দেয়):

const paidOrders = orders.filter((id) => paidIds.includes(id));

এক লাইন, পরিষ্কার — আর ১০টি অর্ডারে একদম ঠিক। কিন্তু includes নিজেও একটি লুপ: সে paidIds-এর শুরু থেকে একটি একটি করে মেলায়। অর্থাৎ একটি লুপের ভেতরে আরেকটি লুপ। জিনিসের সংখ্যা n হলে কাজ বাড়ে প্রায় n × n হারে — time complexity-র ভাষায় O(n²). সমাধান পর্ব ১৮-এর Set: একবার Set বানালে প্রতিটি has() গড়ে প্রায় এক ধাপে উত্তর দেয়, কারণ ভেতরে আছে hash table.

কাজের হিসাব (paidIds-এ একই ১,০০০ id, উল্টো ক্রমে)
অবস্থা
includes-লুপ
Set
বৃদ্ধির ধরন
  1. সমস্যা। ১,০০০টি অর্ডার, ১,০০০টি পরিশোধিত id — ধরা যাক সবগুলোই পরিশোধিত, কিন্তু গেটওয়ে তালিকাটি উল্টো ক্রমে দিয়েছে। প্রশ্ন: কোন পদ্ধতিতে কত ধাপ লাগে?
  2. প্রথম অর্ডারেই ১,০০০ তুলনা। id 1 তালিকার একদম শেষে, তাই includes সব ১,০০০টি দেখে তবে পায়। id 2-এর জন্য ৯৯৯টি… শেষ id-র জন্য মাত্র ১টি।
  3. মোট প্রায় ৫ লাখ। যোগফল ১ + ২ + … + ১,০০০ = ৫,০০,৫০০ তুলনা। গড়ে প্রতিটি অর্ডারে তালিকার অর্ধেক — n²/২.
  4. n দ্বিগুণ, কাজ চার গুণ। পরের মাসে ২,০০০ অর্ডার হলে প্রায় ২০ লাখ তুলনা; ১০,০০০ হলে ৫ কোটি। ডেটা একটু বাড়লেই পাতা হঠাৎ “হ্যাং”। আজকের দ্রুত ল্যাপটপে টের পাবেন না — বিপদ লুকিয়ে থাকে ভবিষ্যতে।
  5. Set: মাত্র ২,০০০ ধাপ। একবার ১,০০০টি id Set-এ ঢোকানো, তারপর ১,০০০টি has(). মোট ২,০০০ — প্রায় ২৫০ গুণ কম কাজ। n দ্বিগুণ হলে কাজও শুধু দ্বিগুণ। কোনো for বনাম forEach কৌশল এর ধারেকাছেও যেতে পারে না।

নিজে চালিয়ে দেখুন। এখানে সময় মাপা হচ্ছে না (সময় যন্ত্র অনুযায়ী বদলায়), গোনা হচ্ছে কাজ — কয়টি তুলনা বা ধাপ লাগল:

function countWork(n) {
  const orders = Array.from({ length: n }, (_, i) => i + 1);
  const paid = orders.toReversed();            // একই id, উল্টো ক্রমে
  let slow = 0;                                // includes-এর মতো খোঁজা
  for (const id of orders) {
    for (const p of paid) { slow++; if (p === id) break; }
  }
  let fast = 0, found = 0;
  const paidSet = new Set();
  for (const p of paid) { paidSet.add(p); fast++; }      // একবার Set বানানো
  for (const id of orders) { fast++; if (paidSet.has(id)) found++; }
  return { slow, fast, found };
}
const fmt = (x) => x.toLocaleString("en-US");
for (const n of [100, 1000, 10000]) {
  const { slow, fast, found } = countWork(n);
  console.log(`n=${fmt(n)} → includes-লুপ: ${fmt(slow)} তুলনা · Set: ${fmt(fast)} ধাপ · মিলেছে ${fmt(found)}`);
}
const r = countWork(1000);
console.log("n=1,000-এ কাজ কমেছে প্রায়", Math.round(r.slow / r.fast), "গুণ");

আউটপুট: n=100 → includes-লুপ: 5,050 তুলনা · Set: 200 ধাপ · মিলেছে 100, তারপর n=1,000-এ 500,500 বনাম 2,000, আর n=10,000-এ 50,005,000 বনাম 20,000; শেষ লাইন n=1,000-এ কাজ কমেছে প্রায় 250 গুণ. ভেতরের for…of লুপটি includes যা করে ঠিক তাই করে, শুধু প্রতিটি তুলনা গোনে। লক্ষ করুন, n দশ গুণ হলে বাঁ দিকের সংখ্যা প্রায় একশো গুণ হয়, ডান দিকেরটা দশ গুণ — O(n²) বনাম O(n)-এর চেহারা। দুই পদ্ধতিই একই উত্তর দেয়; পার্থক্য শুধু কাজে।

Memoization — একই হিসাব দ্বিতীয়বার নয়

পর্ব ২২-এ দেখা memoization: একটি pure function-এর ফল input অনুযায়ী মনে রাখা, যাতে একই input-এ আবার হিসাব না লাগে। যেমন একটি ড্যাশবোর্ড প্রতিবার আঁকার সময় একই মাসের বিক্রির সারাংশ নতুন করে হিসাব করে — memoize করলে মাসপ্রতি একবার। কিন্তু memoization বিনামূল্যে নয়: cache মেমরি খায়, সস্তা হিসাবে cache খোঁজাই বেশি দামি হতে পারে, আর সীমাহীন cache নিজেই memory leak (অংশ ৬)। নিয়ম: profiler-এ যে হিসাব বারবার আর ভারী দেখায়, শুধু সেটাই memoize করুন।

V8-বান্ধব কোড — জানুন, কিন্তু মাথায় চড়াবেন না

পর্ব ০৫-এ দেখেছেন, V8 একই আকারের object-এর জন্য একটি hidden class বানায়, আর inline cache দিয়ে property খোঁজা দ্রুত করে। কিছু অভ্যাস engine-কে সাহায্য করে: সব object একই ক্রমে একই property দিয়ে তৈরি করা (constructor বা class-এ), গরম লুপের object থেকে delete না করা (property-তে undefined বসানো ভালো, কারণ delete object-কে ধীর “dictionary” ধাঁচে নিয়ে যেতে পারে), আর একটি function-কে সবসময় একই ধরনের মান দিয়ে ডাকা — যাকে বলে monomorphic. এগুলো ভাঙলে deoptimization হতে পারে। তবে এর প্রভাব চোখে পড়ে শুধু লাখোবার চলা গরম কোডে; সাধারণ অ্যাপে বড় লাভ থাকে bundle size, rendering আর অ্যালগরিদমে। Micro-optimization শেষ ধাপ, প্রথম নয়।

৬ · Memory — garbage collector কী মোছে, আর কী মুছতে পারে না

পর্ব ০৫ আর ১৮-এ দেখেছেন, JavaScript-এ মেমরি নিজে মুক্ত করতে হয় না — garbage collector (GC) তা করে। GC-র নিয়ম একটাই: reachability. Root (global object, call stack-এর variable, চলমান timer আর listener) থেকে reference ধরে ধরে যে object-এ পৌঁছানো যায়, সেটি জীবিত; যেটিতে যায় না, সেটি মুছে ফেলা হয়। Heap-কে V8 ভাগ করে রাখে দুই প্রজন্মে — একে বলে generational GC. বেশিরভাগ object জন্মের অল্প পরেই অপ্রয়োজনীয় হয় (একটি function-এর ভেতরের অস্থায়ী array, একটি request-এর ডেটা)। তাই নতুন object-এর ছোট জায়গাটি ঘন ঘন আর দ্রুত পরিষ্কার হয়; যারা কয়েকবার টিকে যায়, তারা পুরোনো প্রজন্মে যায়, যা কম ঘন ঘন পরিষ্কার হয়। আধুনিক V8 এই কাজের বড় অংশ অন্য thread-এ, আপনার কোডের পাশাপাশি করে (ভাবনার প্রশ্ন ৪)।

এখান থেকেই memory leak-এর সংজ্ঞা আসে। JavaScript-এ leak মানে GC-র ভুল নয়; মানে আপনার আর দরকার নেই, কিন্তু কোনো root থেকে এখনো পৌঁছানো যায় এমন জিনিস। GC বুঝতে পারে না আপনার “দরকার” আছে কি না — সে শুধু দেখে পৌঁছানো যায় কি না। Leak ধীরে ধীরে জমে: এক ঘণ্টা পর মেমরি কয়েকশো MB, GC বারবার চলে, পাতা থেমে থেমে চলে। Single-page app আর দিনভর খোলা থাকা ড্যাশবোর্ড বা চ্যাট অ্যাপে এটি সবচেয়ে বেশি দেখা যায়; Node.js সার্ভারে এর মানে কয়েক দিন পর মেমরি শেষ হয়ে crash.

Leak-এর উৎসকেন মুছতে পারে নাসমাধান
ভুলে যাওয়া setIntervalচলমান timer একটি root; তার callback আর callback-এর closure-এ ধরা সব ডেটা জীবিত থাকেকাজ শেষ বা component সরানোর সময় clearInterval(id)
window বা document-এ রাখা listener, যার element আগেই সরানো হয়েছেwindow জীবিত → listener জীবিত → তার closure-এ থাকা পুরোনো element আর ডেটাও জীবিতremoveEventListener, বা AbortController-এর signal দিয়ে একবারে সব (পর্ব ৩৮)
সীমাহীন cache বা Mapmodule-স্তরের Map জীবিত, তাই তার ভেতরের সব entry চিরকাল জীবিতসর্বোচ্চ সীমা (LRU), মেয়াদ (TTL), বা object key হলে WeakMap
বড় ডেটা ধরে রাখা closureএকটি ছোট callback-ও তার জন্মস্থানের variable ধরে রাখতে পারে — যেমন পুরো ৫০ MB-র response (পর্ব ২২)closure-এ শুধু দরকারি অংশ রাখুন; callback-টি নিজেই সরান
Detached DOM nodeDOM থেকে সরানো হলেও JavaScript-এর কোনো array বা variable এখনো element-টি ধরে আছেসরানোর সময় সেই reference-ও মুছুন; element-ভিত্তিক তথ্য WeakMap-এ রাখুন
ভুল করে global variableglobal object একটি root; সেখানে ঝোলানো সব কিছু পাতা বন্ধ না হওয়া পর্যন্ত জীবিতmodule বা "use strict" (অঘোষিত নামে লেখা তখন ReferenceError); global-এ বড় ডেটা নয়

বাস্তব ঘটনা: infinite scroll-এর leak

একটি খবরের অ্যাপে প্রতিটি “পাতা” (route) খুললে একটি তালিকা component তৈরি হয়, যা scroll-এ নতুন খবর আনে। কয়েকবার এদিক-ওদিক ঘোরার পর ফোনে অ্যাপ ধীর হয়ে যায়। কোডটি ছিল এমন:

// ভুল: component সরানো হয়, কিন্তু window-এর listener আর timer থেকে যায়
function mountFeed(container) {
  const cards = [];                                  // সব খবরের কার্ড
  window.addEventListener("scroll", () => {
    if (nearBottom()) cards.push(...appendMoreCards(container));
  });
  setInterval(() => refreshTimes(cards), 60_000);   // "৫ মিনিট আগে" হালনাগাদ
}
// ঠিক: একটি AbortController দিয়ে সব listener, আর timer-এর id রেখে পরিষ্কার
function mountFeed(container) {
  const cards = [];
  const controller = new AbortController();
  window.addEventListener("scroll", () => {
    if (nearBottom()) cards.push(...appendMoreCards(container));
  }, { signal: controller.signal });
  const timer = setInterval(() => refreshTimes(cards), 60_000);
  return function unmount() {                        // route ছাড়ার সময় ডাকুন
    controller.abort();                              // signal-যুক্ত সব listener সরে যায়
    clearInterval(timer);
    cards.length = 0;
  };
}

প্রথম সংস্করণে route বদলালে container DOM থেকে সরে যায়, কিন্তু window-এর scroll listener আর চলমান interval দুটোই root থেকে পৌঁছানো যায়। দুটোর closure ধরে আছে cards array, আর array ধরে আছে শত শত কার্ড element — সেগুলো এখন detached DOM node. পাঁচবার ঘুরলে পাঁচটি listener, পাঁচটি timer, পাঁচ সেট কার্ড। দ্বিতীয় সংস্করণে unmount() সব পরিষ্কার করে; React বা Vue-তে (পর্ব ৫৪) এই কাজ হয় cleanup function-এ। বিপদ সেই listener-এ, যা জীবিত কিছুতে (window, document, global event bus) লাগানো — element-এর নিজের listener element-এর সাথেই মুছে যায়।

বাস্তব ঘটনা: চ্যাট অ্যাপের চিরকাল বাড়তে থাকা cache

একটি চ্যাট অ্যাপ প্রতিটি খোলা কথোপকথনের বার্তা একটি Map-এ রাখত, যাতে আবার খুললে সার্ভারে যেতে না হয়। কিন্তু কিছুই কখনো বের করা হতো না — সারাদিন অ্যাপ খোলা রাখা একজন সাপোর্ট কর্মীর ব্রাউজারে সন্ধ্যার মধ্যে কয়েকশো কথোপকথন জমত। সমাধান: একটি সীমা — LRU cache (Least Recently Used). সীমা পার হলে সবচেয়ে বেশি দিন ব্যবহার না হওয়া entry বাদ যায়। JavaScript-এর Map ঢোকানোর ক্রম মনে রাখে (পর্ব ১৮), তাই LRU বানানো সহজ: ব্যবহারের সময় entry মুছে আবার শেষে বসান, আর বাদ দিন সবচেয়ে প্রথমটি। নিচে দুটি cache পাশাপাশি — সংখ্যাগুলো সময় নয়, size:

class LRUCache {
  #max; #map = new Map();
  constructor(max) { this.#max = max; }
  get(key) {
    if (!this.#map.has(key)) return undefined;
    const value = this.#map.get(key);
    this.set(key, value);                    // মুছে আবার বসানো → সদ্য ব্যবহৃত, শেষে
    return value;
  }
  set(key, value) {
    this.#map.delete(key); this.#map.set(key, value);
    if (this.#map.size > this.#max) this.#map.delete(this.#map.keys().next().value); // সবচেয়ে পুরোনোটি বাদ
  }
  get size() { return this.#map.size; }
}
const forever = new Map();          // ভুল: যা ঢোকে, কখনো বের হয় না
const recent = new LRUCache(50);    // ঠিক: সর্বোচ্চ ৫০টি চ্যাট
let hits = 0;
for (let i = 1; i <= 1000; i++) {
  const messages = Array(200).fill("…");      // একটি চ্যাটের ২০০টি বার্তা
  forever.set(`chat-${i}`, messages); recent.set(`chat-${i}`, messages);
  if (i > 3 && recent.get(`chat-${i - 3}`)) hits++;  // সাম্প্রতিক চ্যাটে ফেরা
  if (i % 250 === 0) console.log(`${i}টি চ্যাট খোলার পর → Map: ${forever.size}, LRU: ${recent.size}`);
}
console.log("সাম্প্রতিক চ্যাটে ফেরা:", hits, "বার — LRU-তে প্রতিবারই পাওয়া গেছে");

আউটপুট: 250টি চ্যাট খোলার পর → Map: 250, LRU: 50, তারপর ৫০০, ৭৫০ আর ১০০০-এ Map যথাক্রমে 500, 750, 1000 কিন্তু LRU প্রতিবার 50; শেষে সাম্প্রতিক চ্যাটে ফেরা: 997 বার — LRU-তে প্রতিবারই পাওয়া গেছে. সাধারণ Map-এর আকার অ্যাপ যতক্ষণ খোলা, ততক্ষণ বাড়ে — ১,০০০ চ্যাটে ২ লাখ বার্তা মেমরিতে। LRU কখনো ৫০ ছাড়ায় না (সর্বোচ্চ ১০,০০০ বার্তা), অথচ মানুষ সাধারণত যে সাম্প্রতিক চ্যাটগুলোতে ফেরেন, সেগুলো সবসময় cache-এ থাকে — ৯৯৭ বারের প্রতিবারই। this.#map.keys().next().value হলো Map-এর সবচেয়ে প্রথম (সবচেয়ে পুরোনো) key. বাস্তবে একটি মেয়াদ (TTL)-ও যোগ করা যায়।

WeakMap আর WeakRef কখন: cache-এর key যদি একটি object হয় (যেমন একটি DOM element বা একটি user object), তবে WeakMap সবচেয়ে পরিষ্কার সমাধান — key-টি অন্য কোথাও আর না থাকলে entry নিজেই মুছে যায়, সীমা রাখার দরকারই পড়ে না (পর্ব ১৮)। String key-র cache-এ WeakMap চলে না; সেখানে LRU বা TTL. WeakRef উন্নত আর বিরল প্রয়োজনের জন্য, কারণ GC কখন চলবে তা অনিশ্চিত।

৭ · DevTools দিয়ে তদন্ত — Memory, Performance, Lighthouse আর আরও

Memory প্যানেল — heap snapshot তুলনা করে leak ধরা

Leak সন্দেহ হলে প্রথম প্রশ্ন: একটি কাজ বারবার করলে মেমরি কি শুধু বাড়ে? Chrome বা Edge-এর DevTools-এ Memory প্যানেলের heap snapshot হলো সেই মুহূর্তে heap-এর সব object-এর একটি ছবি — কোন ধরনের কয়টি object, কত বড়, আর কে কাকে ধরে রেখেছে। একটি snapshot একা খুব কিছু বলে না; শক্তি হলো দুটি snapshot-এর তুলনা। আগের অংশের খবরের অ্যাপের leak এভাবে ধরা পড়েছিল:

DevTools → Memory প্যানেল (সংখ্যাগুলো উদাহরণ)
ধাপ
কী করলাম
যা দেখা গেল
সিদ্ধান্ত
  1. ভিত্তি snapshot. পাতা লোড করে সন্দেহের কাজটি একবার করে নিলাম (প্রথমবারের স্বাভাবিক cache আর lazy-load বাদ দিতে), তারপর Memory প্যানেলে “Heap snapshot” নিলাম। এটাই তুলনার ভিত্তি।
  2. একই কাজ পাঁচবার। feed খোলা, একটু scroll, অন্য পাতায় যাওয়া — পাঁচবার। Leak থাকলে প্রতিবার একই পরিমাণ জিনিস জমবে, আর পাঁচ গুণ হলে সেটা চোখে পড়বে।
  3. দ্বিতীয় snapshot আর তুলনা। আগে ময়লার ঝুড়ির বোতাম (Collect garbage) চেপে GC চালালাম। তারপর snapshot নিয়ে “Comparison” view-তে Snapshot ১-এর সাথে মেলালাম। HTMLDivElement: নতুন ২,০০০, মোছা ০ — প্রতিবারের ৪০০ কার্ড থেকে যাচ্ছে।
  4. Detached node. Class filter-এ “Detached” লিখতেই দেখা গেল ২,০০০টি Detached HTMLDivElement — DOM-এ নেই, কিন্তু JavaScript থেকে এখনো পৌঁছানো যায়। Leak নিশ্চিত।
  5. Retaining path — কে ধরে রেখেছে? একটি detached div বাছাই করলে নিচের Retainers অংশে root পর্যন্ত পথ দেখা যায়: div-কে ধরে আছে cards array, তাকে একটি closure-এর context, সেই closure হলো scroll listener, আর listener লাগানো Window-এ। এই retaining path-ই দোষী দেখিয়ে দেয়।
  6. ঠিক করুন, আবার মাপুন। unmount()-এ controller.abort() আর clearInterval যোগ করে পুরো পরীক্ষা আবার চালালাম: Detached ০, heap ভিত্তির কাছে ফিরেছে।

Memory প্যানেলের Allocation instrumentation on timeline মোড দেখায় কোন মুহূর্তে তৈরি object টিকে রইল — কোন ক্লিকে leak হয় তা ধরতে কাজের। আর প্রথম ধাপ হিসেবে Chrome-এর Task Manager (Shift+Esc) বা Performance প্যানেলের “Memory” চেকবক্স দিয়ে দেখতে পারেন, মেমরির রেখা করাতের দাঁতের মতো ওঠানামা করছে (স্বাভাবিক) নাকি সিঁড়ির মতো শুধু উঠছে (leak-এর লক্ষণ)।

Performance প্যানেল

Record চেপে পাতা ব্যবহার করুন, থামান — DevTools দেখাবে প্রতিটি মিলিসেকেন্ডে main thread কী করছিল। ওপরে সময়ের রেখা, তার নিচে Network, Frames, আর সবচেয়ে গুরুত্বপূর্ণ Main সারি — একটি flame chart: অনুভূমিক দিকে সময়, উল্লম্ব দিকে call stack (ওপরের ব্লক নিচের ব্লকগুলোকে ডেকেছে)। চওড়া ব্লক মানে বেশি সময়। হলুদ = JavaScript, বেগুনি = style আর layout, সবুজ = paint. Long task-এর ওপরের কোণে লাল ত্রিভুজ থাকে। নিচের Bottom-Up ট্যাব বলে কোন function নিজে সবচেয়ে বেশি সময় নিল। নতুন সংস্করণে প্যানেল খুললেই নিজের যন্ত্রে মাপা LCP, CLS আর INP দেখা যায়, পাশে CrUX-এর field data-ও। CPU throttling (৪× বা ৬× ধীর) দিয়ে কম দামের ফোনের মতো অবস্থা তৈরি করুন, আর extension এড়াতে Incognito-তে মাপুন।

আরও তিনটি দরকারি জায়গা

  • Network প্যানেল আর throttling: প্রতিটি ফাইলের আকার (চাপা ও খোলা), সময়, ক্রম আর cache থেকে এল কি না। Network throttling-এর “Slow 4G” বা “3G” বাছলে দুর্বল মোবাইল নেটওয়ার্কে পাতা কেমন, তা নিজের চোখে দেখা যায়।
  • Coverage ট্যাব: Command menu (Ctrl+Shift+P) থেকে “Coverage” খুলে record করলে প্রতিটি JS আর CSS ফাইলের কত অংশ আসলে চলল, তা লাল-সবুজ দাগে দেখায়। প্রথম লোডে ৭০% কোড না চললে সেটা code splitting-এর পরিষ্কার সুযোগ।
  • Lighthouse: এক ক্লিকে performance, accessibility, SEO-র নিরীক্ষা, আর নির্দিষ্ট পরামর্শ — বড় ছবি, render-blocking resource, অব্যবহৃত JavaScript. শুরু করার ভালো জায়গা, তবে সাফল্যের মাপকাঠি field data.

DevTools-এ মাপলে নিজের কোডেও মাপার চিহ্ন বসাতে পারেন। performance.mark() আর performance.measure() দেওয়া অংশগুলো Performance প্যানেলের Timings সারিতে নাম ধরে দেখা যায়:

performance.mark("sort-start");
const sorted = products.toSorted((a, b) => a.price - b.price);
renderCards(sorted);
performance.mark("sort-end");
const m = performance.measure("sort-and-render", "sort-start", "sort-end");
console.log(m.name, Math.round(m.duration), "ms");
// আপনার মেশিনে প্রায়: sort-and-render 412 ms (যন্ত্র আর ডেটা অনুযায়ী বদলাবে)

এই ব্লক চালানোর জন্য নয় — আর ইচ্ছা করেই। সময়ের সংখ্যা প্রতিবার আলাদা হয় — যন্ত্র, অন্য tab, JIT warm-up, GC কখন চলল, সবকিছু প্রভাব ফেলে। তাই একবারের মাপে সিদ্ধান্ত নয়: কয়েকবার মাপুন, মাঝের মান (median) দেখুন, throttling দিয়ে। এই পর্বের চালানো যায় এমন উদাহরণগুলো একই কারণে সময় নয়, কাজ গোনে।

৮ · Node.js-এ profiling আর CI-তে performance budget

সার্ভারেও একই চক্র — মাপুন, খুঁজুন, ঠিক করুন, আবার মাপুন — শুধু টুল আলাদা। সবচেয়ে সহজ পথ হলো Node-কে Chrome DevTools-এর সাথে জুড়ে দেওয়া:

# DevTools-এর সাথে জুড়ে চালান, তারপর Chrome-এ chrome://inspect খুলুন
node --inspect server.js

# পুরো চলার CPU profile ফাইলে লিখুন (.cpuprofile — DevTools-এ খোলা যায়)
node --cpu-prof server.js

# heap snapshot-এর জন্য: DevTools-এর Memory ট্যাব, বা কোডে v8.writeHeapSnapshot()

--inspect দিলে chrome://inspect-এ আপনার Node process দেখা যায়; সেখান থেকে খোলা DevTools-এ ব্রাউজারের মতোই Performance (CPU profile) আর Memory (heap snapshot তুলনা) — একই পদ্ধতিতে Node সার্ভারের leak খোঁজা যায়। --cpu-prof process শেষ হলে বর্তমান folder-এ একটি CPU profile ফাইল রাখে, যা DevTools-এ টেনে আনলে flame chart দেখায়। Clinic.js বা 0x-এর মতো টুলও আছে, যা Node-এর profile থেকে flame graph বানায় আর সাধারণ সমস্যার দিকে ইঙ্গিত দেয়। Node নিয়ে বিস্তারিত পর্ব ৪৯-এ।

Node সার্ভারের স্বাস্থ্যের সবচেয়ে কাজের একটি সংখ্যা হলো event loop lag — একটি timer যখন চলার কথা, তার কত পরে আসলে চলল। পর্ব ৩১-এ দেখেছেন, একটি request-এ ভারী sync কাজ হলে বাকি সব request অপেক্ষা করে; lag বেড়ে যাওয়া তার সরাসরি লক্ষণ। Node-এর perf_hooks module-এর monitorEventLoopDelay() এই delay-র histogram দেয় — p99 মানটি নিয়মিত monitoring-এ পাঠান, আর হঠাৎ বেড়ে গেলে সতর্কবার্তা।

Performance budget — পারফরম্যান্স যেন ধীরে ধীরে না হারায়

পারফরম্যান্স এক দিনে খারাপ হয় না — প্রতি সপ্তাহে একটি “ছোট” library, এক বছর পর পাতা দ্বিগুণ ভারী। সমাধান হলো performance budget: আগে থেকে সীমা ঠিক করা — যেমন “প্রথম পাতার JavaScript সর্বোচ্চ ১৭০ KB (চাপা অবস্থায়)”, “মোবাইলে Lighthouse-এর LCP ২.৫ সেকেন্ডের কম” — আর প্রতিটি pull request-এ CI (স্বয়ংক্রিয় build ও test-এর ব্যবস্থা; টেস্টিং নিয়ে পর্ব ৪৮) সেটা যাচাই করে। size-limit-এর মতো টুল bundle-এর আকার মাপে, Lighthouse CI প্রতিটি build-এ Lighthouse চালায়; সীমা পার হলে build লাল। ফলে প্রশ্নটা রিভিউতেই ওঠে: “এই ৮০ KB-র library কি সত্যিই দরকার?”

C/C++/Python/Java জানলে

C/C++-এ মেমরি নিজে মুক্ত করতে হয়, তাই leak মানে free বা delete ভুলে যাওয়া — Valgrind আর AddressSanitizer দিয়ে ধরা হয় (C-তে ডিবাগিং — gdb, sanitizer আর Valgrind); আধুনিক C++ smart pointer দিয়ে মালিকানা স্বয়ংক্রিয় করে (RAII ও smart pointer). Java আর Python-এ GC আছে, তাই leak হয় JavaScript-এর মতোই — “এখনো পৌঁছানো যায়” এমন cache বা listener থেকে; Java-তে heap dump আর JFR (JVM মেমরি আর garbage collection), Python-এ cProfile আর tracemalloc (Python-এ পারফরম্যান্স)। নীতি সব ভাষায় এক: আগে profiler দিয়ে মাপুন।

পরীক্ষা ও ইন্টারভিউতে
  • Core Web Vitals কী কী, আর ভালো সীমা কত? — LCP ≤ ২.৫ সেকেন্ড, INP ≤ ২০০ ms, CLS ≤ ০.১ — ৭৫তম percentile-এ।
  • JavaScript-এ memory leak কীভাবে হয়, যেখানে GC আছে? — অপ্রয়োজনীয় জিনিস এখনো root থেকে পৌঁছানো যায়: ভুলে যাওয়া timer, window-এর listener, সীমাহীন cache, detached DOM node, global variable.
  • Detached DOM node কী, আর কীভাবে খুঁজবেন? — DOM থেকে সরানো কিন্তু JavaScript-এর reference থেকে জীবিত element; Memory প্যানেলে heap snapshot নিয়ে “Detached” filter আর Retainers দেখে।
  • লুপে includes ব্যবহারে সমস্যা কী? — O(n²); একবার Set বানিয়ে has() দিলে O(n).
  • Code splitting কীভাবে করবেন? — দরকারের সময় import() দিয়ে আনা; bundler সেটাকে আলাদা ফাইল বানায়, শুরুর JavaScript কমে।

৯ · AI-এর যুগে — অনুমানের পরামর্শ বনাম মাপা profile

“আমার পাতা ধীর, দ্রুত করো” — এই অনুরোধে AI সাধারণত একগাদা পরিচিত টিপস দেয়, কারণ আপনার যন্ত্র, ডেটা বা ব্যবহারকারী — কিছুই সে দেখেনি। নিচের ভুলগুলো চিনে রাখুন:

AI যা করেসমস্যা ও করণীয়
forEach-কে for-এ, map-কে হাতে লেখা লুপে বদলানোMicro-optimization; সাধারণত চোখে পড়ার মতো লাভ নেই, পড়ার সুবিধা কমে। আসল বাধা মেপে খুঁজুন
সব function-এ memoization, React-এ সবখানে useMemoPremature memoization — cache-এর মেমরি আর জটিলতা বাড়ে, সস্তা হিসাবে লাভ নেই, সীমাহীন cache leak-ও হতে পারে
একটি ছোট কাজের জন্য বড় library যোগ (তারিখ, deep clone, debounce)bundle size বাড়ে; ব্রাউজারের নিজের Intl, structuredClone বা কয়েক লাইনের নিজস্ব function প্রায়ই যথেষ্ট
setInterval আর window-এর listener যোগ করে, পরিষ্কারের কোড নেইmemory leak আর বারবার চলা কাজ; প্রতিটি যোগের পাশে মোছা — clearInterval, AbortController
লুপের ভেতরে includes, find, indexOfO(n²); বড় ডেটায় Set বা Map
সব ছবিতে loading="lazy"ওপরের LCP ছবিও দেরিতে আসে; LCP ছবি lazy নয়, বরং fetchpriority="high"
console.time-এ একবার মেপে “এটা ৩ গুণ দ্রুত” দাবিJIT, GC আর যন্ত্রের কারণে একবারের মাপ অর্থহীন; কয়েকবার, throttling দিয়ে, profiler-এ মাপুন

সবচেয়ে কার্যকর উপায় হলো AI-কে অনুমান করতে না দিয়ে আপনার মাপা তথ্য দেওয়া — Performance প্যানেলের Bottom-Up তালিকা, long task-এর বিবরণ, heap snapshot-এর Comparison বা retaining path, Lighthouse-এর রিপোর্ট। নতুন DevTools-এ profile সংরক্ষণ (JSON) করে বা দরকারি অংশ কপি করে দিতে পারেন:

আমার ওয়েব পাতার একটি পারফরম্যান্স সমস্যা বিশ্লেষণ করো। অনুমান নয় — শুধু নিচের মাপা তথ্যের ভিত্তিতে। সমস্যা: [যেমন: মোবাইলে "দাম অনুযায়ী সাজান" ক্লিকে INP p75 = ৬৫০ ms] পরিবেশ: [ব্রাউজার, CPU throttling, ডেটার আকার] Performance প্যানেলের Bottom-Up (শীর্ষ ১০টি function ও self time): [এখানে পেস্ট] প্রাসঙ্গিক কোড: [এখানে পেস্ট] ১. এই profile অনুযায়ী সবচেয়ে বড় বাধা কোনটি, আর কত শতাংশ সময় সেখানে যায়? সংখ্যা উদ্ধৃত করে বলো। ২. শুধু সেই বাধার জন্য ১–২টি সমাধান দাও, আর প্রতিটির কারণ বলো (rendering, layout, অ্যালগরিদম, নেটওয়ার্ক?)। ৩. for বনাম forEach-এর মতো micro-optimization বা নতুন library প্রস্তাব করবে না, যদি না profile সেটা সমর্থন করে। ৪. কোনো timer, listener বা cache যোগ করলে তার পরিষ্কারের (cleanup) কোডও দাও। ৫. ঠিক করার পর কী মেপে আমি প্রমাণ করব যে উন্নতি হয়েছে, সেটা বলো। ৬. কোনো দাবিতে নিশ্চিত না হলে স্পষ্ট বলো।

AI-এর পারফরম্যান্স পরামর্শ হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১“Premature optimization is the root of all evil” — Donald Knuth আসলে পুরো কথাটা কী বলেছিলেন, আর কেন এর অর্ধেক উদ্ধৃতি ভুল বার্তা দেয়?

লাইনটি এসেছে ১৯৭৪ সালে ACM Computing Surveys-এ প্রকাশিত Donald Knuth-এর প্রবন্ধ “Structured Programming with go to Statements” থেকে। তখন বিতর্ক চলছিল: Dijkstra-র “goto ক্ষতিকর” চিঠির পর কোড কি শুধু কাঠামোবদ্ধ হবে, নাকি দক্ষতার জন্য কখনো goto-ও চলবে? Knuth প্রবন্ধের বড় অংশ জুড়ে দেখান কীভাবে সতর্কভাবে দক্ষ কোড লেখা যায় — অর্থাৎ তিনি দক্ষতার বিরুদ্ধে ছিলেন না, বরং খুব যত্ন নিতেন। পুরো অনুচ্ছেদটির মূল কথা মোটামুটি এই: প্রোগ্রামাররা প্রোগ্রামের অগুরুত্বপূর্ণ অংশের গতি নিয়ে ভেবে বা দুশ্চিন্তা করে বিপুল সময় নষ্ট করেন, আর debugging ও রক্ষণাবেক্ষণের কথা ভাবলে এই চেষ্টা আসলে বেশ ক্ষতিকর। “We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.”

শেষ বাক্যটিই সবচেয়ে বেশি বাদ পড়ে। Knuth বলছেন ছোটখাটো দক্ষতা প্রায় ৯৭% সময় ভুলে যান — কিন্তু সেই গুরুত্বপূর্ণ ৩% কখনো ছাড়বেন না। আর কোনটা সেই ৩%? একই প্রবন্ধে তিনি সতর্ক করেন যে প্রোগ্রামারদের অনুমান প্রায়ই ভুল হয়, তাই measurement টুল দিয়ে আগে সেই গুরুত্বপূর্ণ অংশ চিহ্নিত করা উচিত। অর্থাৎ বার্তাটি হুবহু এই পর্বের চক্র: মাপুন, তারপর যেখানে সময় যায় সেখানে পুরো মনোযোগ দিন।

অর্ধেক উদ্ধৃতি তাই দুই দিকে ক্ষতি করে। একদল একে অজুহাত বানায় — “এখন optimization নয়” বলে O(n²) অ্যালগরিদম, ২ MB-র bundle বা ভুলে যাওয়া listener রেখে দেয়, যদিও এগুলো “ছোটখাটো দক্ষতা” নয়, নকশার বড় সিদ্ধান্ত। আরেকদল প্রতিটি লুপে for বনাম forEach নিয়ে তর্ক করে — ঠিক সেই ৯৭%-এর অপচয়। “Premature” শব্দটির মানে “তথ্য ছাড়া, আগেভাগে” — “কখনো নয়” নয়। সঠিক data structure বাছা, অপ্রয়োজনীয় library না নেওয়া, পরিষ্কারের কোড লেখা — এগুলো premature নয়, এগুলো সাধারণ ভালো প্রকৌশল।

মূল উপলব্ধি: Knuth-এর পরামর্শ “optimize করবেন না” নয় — “মেপে দেখুন কোথায় করতে হবে, আর সেখানে পুরো শক্তি দিন”। বড় নকশাগত পছন্দ শুরু থেকেই সঠিক রাখুন; ছোটখাটো কৌশল রাখুন profiler-এর প্রমাণের জন্য।

প্র ০২RAIL (২০১৫) থেকে Core Web Vitals (২০২০), আর FID থেকে INP (২০২৪) — Google কেন বারবার পারফরম্যান্সের মাপকাঠি বদলেছে?

২০১৫ সালে Google-এর Chrome দলের Paul Irish আর Paul Lewis প্রস্তাব করেন RAIL মডেল — Response, Animation, Idle, Load. এর মূল কথা ছিল ব্যবহারকারীকেন্দ্রিক বাজেট: ইনপুটের সাড়া ১০০ ms-এর মধ্যে (তাই কাজের টুকরো ৫০ ms-এর মধ্যে, যেখান থেকে long task-এর সংজ্ঞা), অ্যানিমেশনে প্রতি ফ্রেম প্রায় ১৬ ms (সেকেন্ডে ৬০ ফ্রেম), ফাঁকা সময়ে ছোট টুকরোয় কাজ, আর দ্রুত লোড। RAIL চিন্তার কাঠামো হিসেবে চমৎকার ছিল, কিন্তু এটি ছিল ডেভেলপারদের জন্য নির্দেশিকা — আসল ব্যবহারকারীদের থেকে মাপার মতো একক, তুলনাযোগ্য সংখ্যা নয়। তখন বহু মেট্রিক চালু ছিল (onload, Speed Index, First Meaningful Paint, Time to Interactive…), যার অনেকগুলো ব্যাখ্যা করা বা field-এ মাপা কঠিন।

২০২০ সালের মে মাসে Google ঘোষণা করে Core Web Vitals — মাত্র তিনটি মেট্রিক (LCP, FID, CLS), প্রতিটি আসল ব্যবহারকারীর যন্ত্রে মাপা যায়, প্রতিটির স্পষ্ট সীমা, আর বিচার ৭৫তম percentile-এ। পরে এগুলো Google Search-এর page experience সংকেতের অংশ হয়, ফলে ব্যবসাগুলোর কাছেও গুরুত্ব পায়। ছোট আর সরল তালিকা বেছে নেওয়াটা ছিল ইচ্ছাকৃত: দশটি মেট্রিক কেউ মনে রাখে না, তিনটি রাখে।

কিন্তু FID-এর একটি বড় দুর্বলতা ধরা পড়ে। এটি মাপত শুধু প্রথম interaction-এর input delay — handler শুরু হওয়ার আগের অপেক্ষা। handler নিজে ৮০০ ms নিলেও, বা পরের দশটি ক্লিক ধীর হলেও FID ভালো দেখাত। Field data-য় দেখা গেল বেশিরভাগ সাইট FID-এ “ভালো”, অথচ ব্যবহারকারীরা ধীরগতির অভিযোগ করছেন। তাই কয়েক বছর পরীক্ষামূলকভাবে চালানোর পর ২০২৪ সালের মার্চে INP FID-এর জায়গা নেয় — যা পুরো ভিজিটের সব interaction দেখে, আর input delay, processing, presentation তিনটিই গোনে। সাইটগুলোর একটি বড় অংশ INP-তে FID-এর চেয়ে খারাপ ফল পায় — কারণ মাপকাঠি সত্যের কাছাকাছি এসেছে।

মূল উপলব্ধি: প্রতিটি মেট্রিক বাস্তবতার একটি অসম্পূর্ণ ছবি, আর মানুষ সংখ্যাটা “ঠিক” করতে শেখে, অনুভূতি নয় (Goodhart-এর সূত্র)। তাই মাপকাঠি বদলায়। আপনার লক্ষ্য কোনো একটি সংখ্যা নয় — ব্যবহারকারীর অভিজ্ঞতা; মেট্রিক শুধু তার থার্মোমিটার।

প্র ০৩ওয়েব পাতার JavaScript বছরের পর বছর বেড়েই চলেছে। এই “JavaScript bloat” কেন সবার জন্য সমান সমস্যা নয় — Alex Russell-এর “performance inequality gap” কী?

HTTP Archive প্রতি মাসে কয়েক লাখ (এখন কয়েক মিলিয়ন) ওয়েবসাইট স্বয়ংক্রিয়ভাবে লোড করে মেপে রাখে — ২০১০ সাল থেকে। তাদের তথ্যে একটি প্রবণতা স্পষ্ট: একটি সাধারণ (median) পাতা যত JavaScript পাঠায়, তা এক দশকে কয়েক গুণ বেড়েছে, আর এখন মোবাইলেও কয়েকশো KB (চাপা অবস্থায়) স্বাভাবিক; বড় অ্যাপে কয়েক MB. কারণ: framework, analytics, বিজ্ঞাপন, chat widget — প্রতিটি দল নিজের অংশ যোগ করে, আর কেউ মোটটা দেখে না।

Google-এর Chrome দলে দীর্ঘদিন কাজ করা Alex Russell কয়েক বছর ধরে একটি ধারাবাহিক লেখায় এর পরিণাম তুলে ধরেছেন, যার নাম “The Performance Inequality Gap”. তাঁর যুক্তি: ডেভেলপাররা কাজ করেন দামি iPhone বা MacBook-এ, দ্রুত Wi-Fi-তে। অথচ পৃথিবীর বেশিরভাগ ইন্টারনেট ব্যবহারকারীর হাতে কম ও মাঝারি দামের Android ফোন — যার CPU দামি ফোনের চেয়ে কয়েক গুণ ধীর, আর এই পার্থক্য সময়ের সাথে কমছে না, বরং দামি ফোন আরও দ্রুত এগিয়ে যাচ্ছে। একই ৫০০ KB JavaScript ডেভেলপারের যন্ত্রে এক মুহূর্তের কাজ, কিন্তু একজন সাধারণ ব্যবহারকারীর ফোনে কয়েক সেকেন্ডের parse আর চালানো। তাই তিনি প্রতি বছর একটি “গড় যন্ত্র আর গড় নেটওয়ার্ক” ধরে হিসাব করে বলেন, প্রথম লোডে মোটামুটি কত KB JavaScript পাঠানো যুক্তিসঙ্গত — আর সেই বাজেট প্রায়ই আজকের median পাতার চেয়ে অনেক ছোট।

বাংলাদেশের প্রেক্ষাপটে কথাটা আরও স্পষ্ট। মোবাইল ডেটার প্যাকেজ কেনা হয় MB হিসাবে, অনেক এলাকায় নেটওয়ার্ক অস্থির, আর বহু মানুষ প্রথম ইন্টারনেট দেখেন কম দামের ফোনে। একটি ভারী পাতা তাঁদের সময় আর টাকা — দুটোই খায়, আর অনেকে অপেক্ষা না করে চলে যান। অর্থাৎ পারফরম্যান্স এখানে শুধু প্রকৌশলের প্রশ্ন নয়, প্রবেশাধিকারের প্রশ্নও।

মূল উপলব্ধি: আপনার যন্ত্র আপনার ব্যবহারকারীর যন্ত্র নয়। প্রতিটি নতুন KB JavaScript-এর দাম সবচেয়ে বেশি দেন সবচেয়ে সাধারণ যন্ত্রের মানুষ — তাই bundle size-কে একটি বাজেট হিসেবে দেখুন, যা খরচ করার আগে ভাবতে হয়।

প্র ০৪“বেশিরভাগ object অল্প বয়সেই মরে” — David Ungar-এর ১৯৮৪ সালের এই পর্যবেক্ষণ কীভাবে আজকের V8-এর GC-কে আকার দিয়েছে, আর GC-র বিরতি কেন এত ছোট হয়ে এসেছে?

প্রথম দিকের garbage collector (১৯৬০-এর দশকে Lisp-এর জন্য John McCarthy-র mark-and-sweep থেকে শুরু) প্রতিবার পুরো heap ঘুরে দেখত, আর সেই সময় প্রোগ্রাম পুরো থেমে থাকত — “stop-the-world”. Heap বড় হলে বিরতিও লম্বা। ১৯৮৪ সালে David Ungar, তখন Berkeley-তে Smalltalk নিয়ে কাজ করছিলেন, “Generation Scavenging” নামে একটি প্রবন্ধে একটি পর্যবেক্ষণ কাজে লাগান, যা পরে generational hypothesis নামে পরিচিত: বেশিরভাগ object জন্মের খুব অল্প পরেই অপ্রয়োজনীয় হয়ে যায়, আর যারা কিছুক্ষণ টিকে যায়, তারা সাধারণত অনেকক্ষণ টেকে। (Lieberman আর Hewitt-ও ১৯৮৩ সালে একই ধারণার একটি রূপ প্রকাশ করেছিলেন।)

এই পর্যবেক্ষণ থেকে নকশা আসে: heap-কে দুই ভাগে ভাগ করা। নতুন object যায় একটি ছোট “young generation”-এ, যা ঘন ঘন পরিষ্কার হয় — আর যেহেতু সেখানে বেশিরভাগই মৃত, শুধু অল্প কয়েকটি জীবিত object কপি করে সরালেই কাজ শেষ; খরচ নির্ভর করে জীবিতের সংখ্যার ওপর, মৃতের নয়। যারা কয়েকবার টিকে যায়, তারা যায় “old generation”-এ, যা অনেক কম ঘন ঘন পরিষ্কার হয়। V8 আজও এই কাঠামো মেনে চলে — young generation-এর জন্য scavenger, old generation-এর জন্য mark-and-sweep আর compaction.

তবু বড় old generation-এর পরিষ্কারে বিরতি লম্বা হতে পারত, যা অ্যানিমেশনে ফ্রেম হারানোর কারণ হতো। তাই V8 দল প্রায় ২০১৬ সাল থেকে Orinoco নামে একটি প্রকল্পে GC-কে ধাপে ধাপে বদলায়: কাজ ছোট টুকরোয় ভাগ করা (incremental), একাধিক helper thread-এ একসাথে করা (parallel), আর আপনার JavaScript চলার পাশাপাশি অন্য thread-এ mark করা (concurrent). ব্রাউজারের ফাঁকা সময়ে (দুটি ফ্রেমের মাঝে) GC-র কাজ করার ব্যবস্থাও আছে। ফলে main thread-এ GC-র বিরতি এখন সাধারণত খুব ছোট।

এর একটি ব্যবহারিক শিক্ষা আছে: অসংখ্য অল্পজীবী object তৈরি করা (যেমন map/filter-এর অস্থায়ী array) সাধারণত সস্তা — young generation ঠিক এর জন্যই বানানো। বিপদ হলো যা টিকে যায় অথচ দরকার নেই — অর্থাৎ leak, যা old generation-কে বাড়িয়ে চলে আর পূর্ণ GC-কে ঘন ঘন ডেকে আনে।

মূল উপলব্ধি: ভালো system নকশা আসে বাস্তব আচরণের পর্যবেক্ষণ থেকে। “অধিকাংশ object অল্পজীবী” — এই একটি তথ্য চার দশক ধরে প্রায় সব আধুনিক GC-র (JVM, .NET, V8) ভিত্তি।

প্র ০৫Amdahl-এর সূত্র পারফরম্যান্সের কাজে কী শেখায়? আর Pinterest বা BBC-র মতো প্রতিষ্ঠানের অভিজ্ঞতা কেন দেখায় যে পারফরম্যান্স ব্যবসারও প্রশ্ন?

১৯৬৭ সালে IBM-এর কম্পিউটার স্থপতি Gene Amdahl একটি যুক্তি দেন, যা মূলত parallel computing নিয়ে হলেও যেকোনো optimization-এ খাটে: একটি অংশকে যত দ্রুতই করুন, মোট উন্নতি সীমিত থাকে সেই অংশ মোট সময়ের কত ভাগ, তা দিয়ে। কোনো অংশ মোট সময়ের ১০% হলে, তাকে অসীম দ্রুত করলেও মোট সময় কমবে মাত্র ১০%. উল্টো দিকে, যে অংশ ৮০% সময় নেয়, তাকে অর্ধেক করলেই মোট সময় ৪০% কমে। তাই optimization-এর প্রথম প্রশ্ন “কীভাবে দ্রুত করব” নয় — “কোথায় সময় যায়”। আর এর উত্তর অনুমানে পাওয়া যায় না; profiler দেয়। এই পর্বের INP তদন্তে সবাই sort-কে দোষী ভেবেছিল, অথচ সেটি ছিল মোটের ৫%-এরও কম।

পারফরম্যান্স যে ব্যবসারও প্রশ্ন, তা কয়েকটি বহুল আলোচিত ঘটনায় দেখা যায় (দাবিগুলো প্রতিষ্ঠানগুলোর নিজেদের প্রকাশিত, তাই প্রেক্ষাপটসহ পড়ুন)। Pinterest ২০১৭ সালে তাদের মোবাইল ওয়েবসাইট নতুন করে PWA হিসেবে বানায় — শুরুর JavaScript ব্যাপকভাবে কমিয়ে, code splitting আর service worker দিয়ে। পরে তারা জানায়, নতুন সাইটে মানুষ উল্লেখযোগ্যভাবে বেশি সময় কাটাচ্ছেন আর বেশি সক্রিয়, যা বিজ্ঞাপনের আয়েও প্রতিফলিত হয়েছিল। BBC-ও প্রকাশ্যে জানিয়েছিল যে তাদের সাইট লোড হতে প্রতিটি বাড়তি সেকেন্ডে ব্যবহারকারীর একটি উল্লেখযোগ্য অংশ চলে যান।

তবে সতর্কতাও দরকার। এসব ঘটনায় প্রায়ই পারফরম্যান্সের সাথে নকশা আর ফিচারও একসাথে বদলেছে, তাই সব উন্নতি শুধু গতির ফল কি না বলা কঠিন। সবচেয়ে নির্ভরযোগ্য প্রমাণ আপনার নিজের: একটি পরিবর্তনের আগে-পরে নিজের RUM আর ব্যবসার সংখ্যা (চেকআউট সম্পন্ন, bounce rate) মিলিয়ে দেখা — সম্ভব হলে A/B test দিয়ে।

মূল উপলব্ধি: সবচেয়ে বড় অংশে আঘাত করুন — Amdahl নিশ্চিত করেন যে ছোট অংশে যত পরিশ্রমই করুন, লাভ সীমিত। আর পারফরম্যান্সের উন্নতি মাপুন ব্যবহারকারীর আচরণে, শুধু millisecond-এ নয়।

অনুশীলন

প্রথমে নিজে ভাবুন বা চালিয়ে দেখুন, তারপর উত্তর মেলান।

  1. কোন মেট্রিক? প্রতিটি অভিযোগে কোন মেট্রিক (LCP, INP, CLS) খারাপ হবে, আর প্রথমে কী দেখবেন — (ক) পণ্যের পাতায় বড় ছবিটি ৫ সেকেন্ড পরে আসে, (খ) পড়ার সময় একটি cookie-নোটিশ ওপরে এসে লেখা নিচে ঠেলে দেয়, (গ) “কার্টে যোগ করুন” চাপার পর এক সেকেন্ড কিছুই হয় না, (ঘ) একটি ড্যাশবোর্ড সকালে দ্রুত, বিকেলে থেমে থেমে চলে।

    (ক) LCP — ছবির আকার আর ফরম্যাট (WebP/AVIF, srcset), ছবিটি lazy কি না, fetchpriority, আর render-blocking script/CSS দেখুন। (খ) CLS — নোটিশের জন্য আগে থেকে জায়গা রাখুন বা নিচে overlay হিসেবে দেখান। (গ) INP — Performance প্যানেলে ক্লিক record করে long task আর তার ভেতরের ভারী অংশ খুঁজুন। (ঘ) কোনো Core Web Vital সরাসরি নয় — সময়ের সাথে ধীর হওয়া memory leak-এর লক্ষণ; Task Manager-এ মেমরি দেখুন, তারপর heap snapshot তুলনা।

  2. Leak খুঁজুন: একটি single-page app-এর ঘড়ি component-এ লেখা আছে function mountClock(el) { setInterval(() => { el.textContent = formatTime(); }, 1000); document.addEventListener("visibilitychange", () => render(el)); }. Component দশবার খোলা-বন্ধ হলে কী থেকে যায়? কীভাবে ঠিক করবেন?

    দশটি চলমান interval আর document-এ দশটি listener — প্রতিটির closure পুরোনো el ধরে আছে, তাই দশটি detached element-ও জীবিত, আর প্রতি সেকেন্ডে দশবার অকারণে কাজ হচ্ছে। ঠিক করতে const id = setInterval(…) আর const ac = new AbortController() রেখে listener-এ { signal: ac.signal } দিন, আর একটি unmount() ফেরত দিন যা clearInterval(id); ac.abort(); করে। Component সরানোর সময় সেটা ডাকুন।

  3. কাজ গুনুন: ৫০০ জন ছাত্রের তালিকা আর ৫০০ জন বৃত্তিপ্রাপ্তের id-র তালিকা। students.filter((s) => winners.includes(s.id)) সবচেয়ে খারাপ অবস্থায় কতটি তুলনা করবে? Set দিয়ে কত ধাপ? ছাত্র ৫,০০০ হলে (বৃত্তিপ্রাপ্তও ৫,০০০) দুই হিসাব কত গুণ বাড়বে?

    সবচেয়ে খারাপ অবস্থায় (কোনো id না মিললে, প্রতিবার পুরো তালিকা) ৫০০ × ৫০০ = ২,৫০,০০০ তুলনা। Set: ৫০০ add + ৫০০ has = ১,০০০ ধাপ। সংখ্যা দশ গুণ হলে includes-এর হিসাব একশো গুণ (২.৫ কোটি), Set-এর মাত্র দশ গুণ (১০,০০০)। অংশ ৫-এর চালানো যায় এমন উদাহরণে paid-কে এমন id দিয়ে ভরে (যেমন i + n + 1) নিজে যাচাই করতে পারেন।

  4. কোড লিখুন (ঐচ্ছিক): অংশ ৬-এর LRUCache-এ একটি evictions counter যোগ করুন, যা গোনে কয়টি entry বাদ পড়ল, আর লুপের শেষে সেটা ছাপুন। ১,০০০ চ্যাট আর সীমা ৫০ হলে সংখ্যাটি কত হওয়া উচিত, আগে হিসাব করে তারপর চালান।

    Class-এ evictions = 0; (public field) যোগ করে set-এর if-কে block বানিয়ে { this.#map.delete(…); this.evictions++; }. প্রতিটি নতুন চ্যাট একটি নতুন key; প্রথম ৫০টির পর প্রতিটি নতুন key একটি পুরোনোকে সরায়। get নতুন key যোগ করে না, তাই বাদ পড়বে ১,০০০ − ৫০ = ৯৫০টি। console.log("বাদ পড়েছে:", recent.evictions) ছাপবে বাদ পড়েছে: 950.

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

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

ক্রমDevTools-এর Memory প্যানেলে একটি memory leak খোঁজার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📸 সন্দেহের কাজটি একবার করে প্রথম heap snapshot নেওয়া
  2. 🔁 একই কাজ কয়েকবার (যেমন ৫ বার) করা
  3. 🗑️ GC চালিয়ে দ্বিতীয় snapshot নেওয়া
  4. ⚖️ Comparison view-তে কোন object জমছে আর “Detached” node আছে কি না দেখা
  5. 🧭 Retainers-এ retaining path ধরে দোষী reference খোঁজা
  6. ✅ ঠিক করে পুরো পরীক্ষা আবার চালিয়ে নিশ্চিত হওয়া

Core Web Vitals-এ INP-এর “ভালো” সীমা কত?

  • ৫০ ms বা কম
  • ১০০ ms বা কম
  • ২০০ ms বা কম
  • ২.৫ সেকেন্ড বা কম

একটি খবরের পাতায় ছবি নামার পর নিচের লেখা হঠাৎ সরে যায়। কোন পরিবর্তন এই সমস্যার সরাসরি সমাধান?

  • সব ছবিতে loading="lazy" দেওয়া
  • <img>-এ width আর height দেওয়া
  • ছবিকে WebP-তে রূপান্তর করা
  • সব script-এ defer দেওয়া

JavaScript-এ garbage collector থাকা সত্ত্বেও memory leak হয় কেন?

  • GC শুধু পাতা বন্ধ হলে চলে
  • GC চক্রাকার (circular) reference মুছতে পারে না
  • const দিয়ে বানানো object কখনো মোছা হয় না
  • অপ্রয়োজনীয় object এখনো কোনো root থেকে পৌঁছানো যায়

দুটি ১০,০০০ উপাদানের array-তে a.filter((x) => b.includes(x)) ধীর। সবচেয়ে কার্যকর পরিবর্তন কোনটি?

  • b থেকে একবার Set বানিয়ে has() দিয়ে খোঁজা
  • filter-এর বদলে for লুপ লেখা
  • includes-এর বদলে indexOf ব্যবহার করা
  • কোডটি Web Worker-এ পাঠানো

Single-page app-এ একটি component সরানো হয়েছে, কিন্তু তার window-এর scroll listener সরানো হয়নি। সবচেয়ে সম্ভাব্য ফল কী?

  • ব্রাউজার listener-টি নিজেই সরিয়ে দেয়, কোনো সমস্যা নেই
  • পরের scroll-এ সাথে সাথে ReferenceError
  • Listener-এর closure যা ধরে আছে (element, ডেটা) তা মুছতে পারে না — memory leak
  • পাতা সাথে সাথে crash করে

একটি পাতার মোট JavaScript কমাতে, যে চার্ট library শুধু একটি ট্যাবে লাগে তার জন্য সবচেয়ে উপযুক্ত কৌশল কোনটি?

  • শুধু library-টি minify করা
  • ট্যাব খোলার সময় dynamic import() দিয়ে আনা
  • script ট্যাগে async দেওয়া
  • চার্টের function memoize করা

Lab data (যেমন Lighthouse) আর field data (যেমন CrUX বা RUM)-র পার্থক্য সম্পর্কে কোন কথাটি সঠিক?

  • Lighthouse-এ ১০০ পেলে আসল ব্যবহারকারীর অভিজ্ঞতাও নিশ্চিতভাবে ভালো
  • Field data মাপা হয় শুধু ডেভেলপারের নিজের কম্পিউটারে
  • Lab test-এ INP সরাসরি মাপা যায়, field-এ যায় না
  • Lab সমস্যা খুঁজতে কাজের, আর field দেখায় আসল ব্যবহারকারীর অভিজ্ঞতা

শব্দকোষ

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

মনে রাখুন

  1. মাপুন → খুঁজুন → ঠিক করুন → আবার মাপুন। Field data বলে কোথায় সমস্যা, profiler বলে কেন। অনুমান আর micro-optimization-এ সময় নষ্ট।
  2. Core Web Vitals: LCP ≤ ২.৫ সেকেন্ড, INP ≤ ২০০ ms, CLS ≤ ০.১ — ৭৫তম percentile-এ; সহায়ক TTFB আর FCP. Lab-এ খুঁজুন, field-এ সাফল্য মাপুন।
  3. JavaScript সবচেয়ে দামি byte: code splitting (import()), tree-shaking, compression, hash-যুক্ত ফাইলে লম্বা cache, হালকা ছবি (WebP/AVIF, srcset, width/height), font-display: swap, আর defer.
  4. Runtime: long task ভাঙুন, layout thrashing এড়ান, লম্বা তালিকা virtualize করুন, debounce/throttle, ভারী হিসাব Worker-এ — আর সবার আগে অ্যালগরিদম: লুপে includes নয়, Set.
  5. Leak = অপ্রয়োজনীয় কিন্তু পৌঁছানো যায়: timer, window-এর listener, সীমাহীন cache, detached node. Heap snapshot তুলনা আর retaining path দিয়ে খুঁজুন; clearInterval, AbortController, LRU আর WeakMap দিয়ে ঠিক করুন।

পরের পর্বে: এতক্ষণ সবকিছু নিজের হাতে করেছেন — DOM বদলানো, listener পরিষ্কার, কী আঁকতে হবে তা ঠিক করা। React, Vue আর Svelte-এর মতো framework এই কাজগুলো কীভাবে সহজ করে, কোন সমস্যা সমাধান করে আর কী দাম নেয় — আর কোনটি কখন বাছবেন।

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

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