এই পর্বে যা শিখবেন
- PWA কী, কেন বাংলাদেশের মতো দামি আর অনিয়মিত মোবাইল ডেটার দেশে এটি বিশেষ কাজের
- Manifest-এর field, HTTPS-এর শর্ত আর ইনস্টলযোগ্যতার (installability) নিয়ম — যা সময়ের সাথে বদলেছে
- Service worker-এর register, install, waiting, activate, fetch — আর “নতুন সংস্করণ আছে” সমস্যার সমাধান
- Cache API, পাঁচটি caching strategy আর কোনটি কখন; অফলাইন পাতা, quota আর সংস্করণ করা cache
- অফলাইনে অর্ডার জমিয়ে পরে পাঠানো (Background Sync), Push notification, Workbox, DevTools দিয়ে ডিবাগ আর AI-এর লেখা service worker যাচাই
১ · PWA কী — যে ওয়েবসাইট অ্যাপের মতো আচরণ করে
লঞ্চে বরিশাল যাচ্ছেন। নদীর মাঝখানে মোবাইলের নেটওয়ার্ক একবার আসে, একবার যায়। একটি সাধারণ ওয়েবসাইট খুললে দেখবেন ব্রাউজারের ডাইনোসর আর “No internet”. অথচ একই সময়ে আরেকটি সাইট খুলল সাথে সাথে — গতকাল লেখা খরচের হিসাব দেখা যাচ্ছে, নতুন খরচ যোগও করা যাচ্ছে, আর ওপরে ছোট্ট লেখা: “অফলাইন — সংযোগ ফিরলে সেভ হবে”. দ্বিতীয় সাইটটি একটি Progressive Web App, সংক্ষেপে PWA.
PWA আলাদা কোনো প্রযুক্তি বা ভাষা নয়। এটি একটি সাধারণ ওয়েবসাইট — HTML, CSS আর JavaScript দিয়েই বানানো — যার সাথে কয়েকটি বাড়তি জিনিস যোগ করা হয়েছে। ফলে সাইটটি তিনটি নতুন ক্ষমতা পায়:
📲 ইনস্টল করা যায়
হোম স্ক্রিনে বা কম্পিউটারের অ্যাপ তালিকায় নিজের আইকন। খুললে ব্রাউজারের ঠিকানার বার ছাড়া, নিজের জানালায় — দেখতে প্রায় সাধারণ অ্যাপের মতো।
⚡ সাথে সাথে খোলে
পাতার কাঠামো (app shell) আগে থেকেই ফোনে জমা থাকে, তাই দ্বিতীয়বার থেকে নেটওয়ার্কের জন্য অপেক্ষা ছাড়াই পর্দা ভরে যায়।
📴 অফলাইনেও চলে
নেটওয়ার্ক দুর্বল বা একেবারে নেই — তবু আগে দেখা ডেটা দেখা যায়, নতুন কাজ জমা থাকে, সংযোগ ফিরলে পাঠানো হয়।
এই ক্ষমতাগুলোর কেন্দ্রে আছে একটি বিশেষ script — service worker. ব্রাউজার একে পাতা থেকে আলাদা চালায়, আর পাতা থেকে নেটওয়ার্কে যত অনুরোধ যায়, সব এর হাত দিয়ে যায়। প্রতিটি অনুরোধে এটি সিদ্ধান্ত নেয় — জমানো কপি থেকে উত্তর দেব, নেটওয়ার্কে পাঠাব, নাকি দুটোই। এমন মধ্যস্থতাকারীকে বলে proxy; service worker হলো JavaScript-এ আপনার নিজের লেখা proxy, যা ব্যবহারকারীর ব্রাউজারের ভেতরেই বসে।
Service worker কোথায় বসে
পাতা → service worker → (cache বা network) → উত্তর ফেরত পাতায়
- পাতা
fetch("/api/products")নিজে জানে না কে উত্তর দিল - Service worker
fetch eventসিদ্ধান্ত নেয় - Cache Storage
caches.match()ফোনে জমানো উত্তর - Network
fetch(request)সার্ভার, যদি পৌঁছানো যায়
fetch ডাকে (পর্ব ৩৫)। মাঝখানে service worker থাকলে উত্তরটি cache থেকে এল না network থেকে, পাতা তা টেরও পায় না।একটি বড় অফিসের রিসেপশন ডেস্ক ভাবুন। কর্মকর্তারা (পাতা) কিছু চাইলে গুদামে (সার্ভার) না গিয়ে রিসেপশনিস্টকে (service worker) বলেন। তাঁর টেবিলের নিচের তাকে (cache) রোজকার ফর্ম আগেই রাখা — চাইলেই দেন। খবরের কাগজ চাইলে আগে হকারের কাছে লোক পাঠান; হকার না এলে গতকালের কাগজটি দেন, সাথে বলেন “আজকেরটি এখনো আসেনি”. আর চেক বা টাকার কাজ? সেটি কখনো তাকে রাখা কপি দিয়ে হয় না — সরাসরি ব্যাংকে। রিসেপশনিস্টের এই নিয়মগুলোই হলো caching strategy, আর নিয়মগুলো লেখেন আপনি।
কেন এটি আমাদের দেশে বিশেষ গুরুত্বপূর্ণ? মোবাইল ডেটা কিনতে হয় টাকা দিয়ে, প্যাকেজের মেয়াদ শেষ হয়, গ্রাম বা নদীপথে সংযোগ বারবার কাটে, আর লোডশেডিংয়ে Wi-Fi রাউটার বন্ধ। যে সাইট প্রতিবার সব ফাইল নতুন করে নামায়, সে ব্যবহারকারীর টাকা আর ধৈর্য দুটোই খরচ করে। যে সাইট দরকারি ফাইল একবার নামিয়ে জমিয়ে রাখে আর অফলাইনেও কাজ চালিয়ে যায়, সে ঠিক এই বাস্তবতার জন্য বানানো।
“Progressive” শব্দটি গুরুত্বপূর্ণ। পুরোনো বা সীমিত ব্রাউজারে PWA একটি সাধারণ ওয়েবসাইট হিসেবেই চলে; যেখানে service worker আর manifest সমর্থিত, সেখানে বাড়তি সুবিধাগুলো যোগ হয়। এটি পর্ব ২-এর progressive enhancement নীতিরই বিস্তার — কাউকে বাদ না দিয়ে, যার ব্রাউজার পারে তাকে বেশি দেওয়া।
অফলাইন ওয়েবের প্রথম বড় চেষ্টা ছিল HTML5-এর AppCache — একটি manifest ফাইলে ফাইলের তালিকা দিলেই ব্রাউজার জমিয়ে রাখত। কিন্তু এর নিয়ম ছিল অনমনীয় আর চমকে ভরা; এর জবাবেই নকশা হয় service worker — নিয়ম ঘোষণার বদলে নিচু স্তরের ক্ষমতা। গল্পটি ভাবনার প্রশ্ন ১-এ।
২ · তিনটি উপাদান — HTTPS, manifest আর ইনস্টলযোগ্যতা
HTTPS — বাধ্যতামূলক, ব্যতিক্রম শুধু localhost
Service worker প্রতিটি অনুরোধ বদলে দিতে পারে — তাই কেউ যদি পথের মাঝে (যেমন খোলা Wi-Fi-তে) একটি ক্ষতিকর service worker ঢুকিয়ে দিতে পারত, সেটি সাইট বন্ধ করার পরও মাসের পর মাস ব্যবহারকারীর সব অনুরোধ চুরি করত। এজন্য ব্রাউজার service worker শুধু secure context-এ চালায়: https:// দিয়ে আসা পাতা, অথবা ডেভেলপারের সুবিধার জন্য http://localhost আর http://127.0.0.1. HTTPS আর HSTS কেন জরুরি, তা পর্ব ৪৩-এ দেখেছেন।
Web app manifest — অ্যাপের পরিচয়পত্র
ইনস্টল হওয়ার পর অ্যাপটির নাম কী দেখাবে, আইকন কোনটি, খুললে কোন পাতা আসবে, ব্রাউজারের বার থাকবে কি না — এসব লেখা থাকে একটি JSON ফাইলে, যার নাম web app manifest. ফাইলটি পাতার <head>-এ যুক্ত করতে হয়:
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#0d6e4f">
<link rel="apple-touch-icon" href="/icons/192.png"> <!-- iPhone/iPad-এর হোম স্ক্রিন আইকন -->{
"name": "অফলাইন খাতা — খরচের হিসাব",
"short_name": "খাতা",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0d6e4f",
"lang": "bn",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}| Field | কাজ | খেয়াল রাখুন |
|---|---|---|
name / short_name | পুরো নাম (ইনস্টলের সময়, splash পর্দায়) / ছোট নাম (আইকনের নিচে) | ছোট নাম লম্বা হলে হোম স্ক্রিনে কেটে যায় — ১২ অক্ষরের মধ্যে রাখা প্রচলিত পরামর্শ |
start_url | আইকন চাপলে কোন ঠিকানা খুলবে | এই পাতাটি অফলাইনেও খোলা চাই — তাই precache-এ রাখুন |
display | standalone = নিজের জানালা, ঠিকানার বার নেই; minimal-ui, fullscreen; browser = সাধারণ ট্যাব | browser দিলে অ্যাপের মতো খুলবে না |
icons | বিভিন্ন আকারের আইকন | অন্তত 192×192 আর 512×512; Android-এর গোল/চৌকো ছাঁচের জন্য "purpose": "maskable" |
theme_color / background_color | জানালার ওপরের বারের রং / খোলার মুহূর্তের splash পর্দার রং | পাতার <meta name="theme-color">-এর সাথে মিলিয়ে রাখুন |
scope | কোন পথগুলো “অ্যাপের ভেতরে” ধরা হবে | এর বাইরের লিংক খুললে ব্রাউজারে যায় |
ইনস্টলযোগ্যতা — নিয়ম বদলায়, তাই সাবধানে
ব্রাউজার কখন “Install app” বা “হোম স্ক্রিনে যোগ করুন” প্রস্তাব দেবে, তার নিয়মকে বলে installability criteria. এখানে সতর্ক থাকুন — নিয়মগুলো ব্রাউজারভেদে আলাদা আর বছরে বছরে বদলেছে:
- Chrome/Edge (Chromium): HTTPS, আর একটি manifest যাতে
nameবাshort_name, 192px ও 512px আইকন,start_urlআর উপযুক্তdisplayআছে। আগে fetch handler-সহ service worker-ও বাধ্যতামূলক ছিল; সাম্প্রতিক সংস্করণে সেই শর্ত তুলে নেওয়া হয়েছে, আর এখন মেনু থেকে প্রায় যেকোনো সাইটই অ্যাপ হিসেবে যোগ করা যায়। তবে অফলাইনে চলার জন্য service worker তো লাগবেই। - Safari (iPhone/iPad): কোনো স্বয়ংক্রিয় প্রস্তাব নেই; ব্যবহারকারীকে নিজে Share → “Add to Home Screen” চাপতে হয়। Manifest পড়ে, তবে আইকনের জন্য
apple-touch-iconদেওয়া এখনো নিরাপদ। macOS-এ Safari 17 থেকে “Add to Dock”. - Firefox: Android-এ হোম স্ক্রিনে যোগ করা যায়; ডেস্কটপে দীর্ঘদিন ইনস্টলের সুবিধা ছিল না।
তাই কোনো tutorial বা AI-এর “এগুলো থাকলেই ইনস্টল হবে” তালিকা কোন বছরের, কোন ব্রাউজারের — যাচাই করুন; সবচেয়ে বিশ্বাসযোগ্য হলো DevTools-এর Application → Manifest প্যানেল।
নিচের কোডে একটি ছোট manifest যাচাইকারী বানানো হয়েছে, যা প্রচলিত শর্তগুলো পরখ করে। সাথে দেখুন feature detection-এর একটি সূক্ষ্ম ফাঁদ — এই কোড-বক্সের sandbox-এ service worker-এর অনুমতি নেই:
function checkManifest(m) {
const problems = [];
if (!m.name && !m.short_name) problems.push("name বা short_name নেই");
if (!m.start_url) problems.push("start_url নেই");
if (!["standalone", "fullscreen", "minimal-ui"].includes(m.display))
problems.push(`display "${m.display}" — অ্যাপের মতো খুলবে না`);
const sizes = (m.icons ?? []).flatMap((icon) => icon.sizes.split(" "));
for (const s of ["192x192", "512x512"]) if (!sizes.includes(s)) problems.push(`${s} আইকন নেই`);
if ((m.short_name ?? m.name ?? "").length > 12) problems.push("ছোট নাম ১২ অক্ষরের বেশি");
return problems.length ? problems.join("; ") : "ঠিক আছে";
}
const khata = { name: "অফলাইন খাতা", short_name: "খাতা", start_url: "/?source=pwa", display: "standalone",
icons: [{ src: "/i/192.png", sizes: "192x192" }, { src: "/i/512.png", sizes: "512x512" }] };
console.log("খাতা:", checkManifest(khata));
console.log("দোকান:", checkManifest({ name: "Rahim Store Online", display: "browser",
icons: [{ src: "/logo.png", sizes: "192x192" }] }));
console.log("navigator-এ আছে?", "serviceWorker" in navigator);
try { navigator.serviceWorker.register("/sw.js"); }
catch (e) { console.log("কিন্তু ব্যবহার করা গেল না:", e.name); }প্রথম লাইন খাতা: ঠিক আছে. দ্বিতীয় লাইনে একসাথে চারটি সমস্যা: start_url নেই, display "browser", 512x512 আইকন নেই, আর short_name না থাকায় পুরো নাম “Rahim Store Online” (১৮ অক্ষর) হোম স্ক্রিনে কেটে যাবে। শেষ দুই লাইন feature detection-এর শিক্ষা: "serviceWorker" in navigator দিল true — property-টি আছে — কিন্তু ব্যবহার করতে গেলেই Edge/Chrome ছুড়ল SecurityError, কারণ এই কোড চলে একটি sandbox করা iframe-এ, যার origin “opaque” (কোনো নির্দিষ্ট ঠিকানা নেই), আর সেখানে service worker নিষিদ্ধ। শিক্ষা: “আছে কি না” দেখাই যথেষ্ট নয়; আসল কাজটি try/catch (আর Promise হলে .catch) দিয়ে মুড়ে রাখুন। আসল service worker তাই এই পর্বে চালানো যাবে না — নিচে আলাদা ফাইল হিসেবে দেখানো হয়েছে, আর অংশ ৮-এ নিজের কম্পিউটারে চালানোর ধাপ আছে।
৩ · Service worker-এর জীবনচক্র — register, install, activate
Service worker একটি আলাদা ফাইল — সাধারণত সাইটের মূলে /sw.js. পাতার কাজ শুধু ব্রাউজারকে বলা: “এই ফাইলটিকে আমার service worker হিসেবে নিবন্ধন করো।” এই নিবন্ধনের নাম register:
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => { // পাতা আগে লোড হোক, তারপর
try {
const reg = await navigator.serviceWorker.register("/sw.js");
console.log("SW নিবন্ধিত, scope:", reg.scope); // https://khata.example/
} catch (err) {
console.warn("SW নিবন্ধন ব্যর্থ — সাইট সাধারণভাবেই চলবে", err);
}
});
}খেয়াল করুন তিনটি জিনিস। এক, পুরো কাজটি একটি if-এর ভেতরে — service worker না থাকলে সাইট সাধারণ ওয়েবসাইট হিসেবেই চলে। দুই, load-এর পর নিবন্ধন, যাতে প্রথম লোডের ব্যান্ডউইথে ভাগ না বসে। তিন, ফাইলটির অবস্থান ঠিক করে তার scope — কোন পাতাগুলো সে নিয়ন্ত্রণ করবে। /sw.js পুরো সাইট নিয়ন্ত্রণ করে; কিন্তু /js/sw.js ডিফল্টভাবে শুধু /js/-এর নিচের পাতা। (সার্ভার Service-Worker-Allowed header পাঠালে scope বাড়ানো যায়, তবে মূলে রাখাই সহজ।) এখানে “scope” মানে JavaScript-এর ভ্যারিয়েবলের scope নয় — URL-এর পরিধি।
এবার sw.js-এর ভেতরটা। Service worker কোনো DOM দেখে না, window নেই — তার global object self (একটি ServiceWorkerGlobalScope). পর্ব ৪২-এর Web Worker-এর মতোই এটি পাতার main thread-এর বাইরে চলে, কিন্তু উদ্দেশ্য আলাদা: Web Worker ভারী হিসাব সরিয়ে নেয় আর পাতার সাথে বাঁচে-মরে; service worker নেটওয়ার্কের দারোয়ান, একটি origin-এর সব ট্যাবের জন্য একটিই, আর পাতা বন্ধ থাকলেও event এলে ব্রাউজার তাকে জাগিয়ে তোলে। কাজ শেষে ব্রাউজার তাকে থামিয়েও দেয়, তাই global ভ্যারিয়েবলে কিছু জমানো নিরাপদ নয়।
const VERSION = "v3";
const SHELL = `khata-shell-${VERSION}`; // সংস্করণ নামের ভেতরেই
const RUNTIME = "khata-runtime";
const SHELL_FILES = ["/", "/index.html", "/app.3f9a1c.css", "/app.8b2e7d.js",
"/offline.html", "/icons/192.png", "/manifest.webmanifest"];
self.addEventListener("install", (event) => {
// সব ফাইল জমা না হওয়া পর্যন্ত install শেষ হবে না; একটি ব্যর্থ হলে পুরো install ব্যর্থ
event.waitUntil(caches.open(SHELL).then((cache) => cache.addAll(SHELL_FILES)));
});
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
for (const name of await caches.keys()) {
if (name !== SHELL && name !== RUNTIME) await caches.delete(name); // পুরোনো সংস্করণ মোছা
}
})());
});অ্যাপের যে ন্যূনতম কাঠামো — HTML, CSS, JavaScript, আইকন, অফলাইন পাতা — যা ছাড়া পর্দাই আঁকা যায় না, তাকে বলে app shell. Install-এর সময় এগুলো আগে থেকে জমিয়ে রাখাকে বলে precache. event.waitUntil(promise) ব্রাউজারকে বলে “এই Promise শেষ না হওয়া পর্যন্ত আমাকে থামিও না, আর এর ফল দিয়েই ঠিক করো ধাপটি সফল কি না।” এবার প্রথমবার সাইটে আসা একজন ব্যবহারকারীর দিক থেকে পুরো জীবনচক্রটি দেখুন:
- Register
register("/sw.js")পাতা থেকে - Install
cache.addAll(...)app shell জমানো - Activate
caches.delete(old)পুরোনো cache মোছা - এই পাতা?
controller === nullএখনো নিয়ন্ত্রণের বাইরে - পরের লোড
fetch eventসব অনুরোধ SW-এর হাতে
navigator.serviceWorker.register("/sw.js") // ব্রাউজার: ফাইল নামাও → parse করো → একবার উপর থেকে নিচে চালাও // (addEventListener-গুলো এখানেই বসে) // syntax error থাকলে register-এর Promise reject — সাইট সাধারণভাবে চলে
caches.open("khata-shell-v3")
.then(c => c.addAll(["/", "/app.3f9a1c.css", "/offline.html", …]))
// ৭টির মধ্যে ১টিও 404 হলে → install ব্যর্থ, SW বাতিল
// পরের ভিজিটে ব্রাউজার আবার চেষ্টা করবে
caches.keys() // ["khata-shell-v3", "khata-runtime"] // প্রথম ভিজিটে মোছার কিছু নেই; পরের সংস্করণে v2-এর cache এখানে যাবে // পুরোনো SW এখানে কোনো ট্যাব চালাচ্ছে না, তাই মোছা নিরাপদ
navigator.serviceWorker.controller // null // পাতাটি SW ছাড়াই লোড হয়েছিল — মাঝপথে নিয়ন্ত্রণ বদলায় না // চাইলে activate-এ: self.clients.claim() → এখনই নিয়ন্ত্রণ
self.addEventListener("fetch", (event) => {
event.respondWith(caches.match(event.request) …)
});
// HTML, CSS, ছবি, fetch() — এই scope-এর সব অনুরোধ এখানে আসে
// নেটওয়ার্ক বন্ধ থাকলেও app shell cache থেকে খোলে
- Register. ব্রাউজার
sw.jsনামিয়ে একবার পুরোটা চালায় — তখনই event listener-গুলো বসে। ফাইলে syntax error থাকলে নিবন্ধন ব্যর্থ, কিন্তু সাইট আগের মতোই চলে। - Install — app shell জমানো। প্রতিটি সংস্করণে একবারই
installঘটে, আর এখানেই precache.cache.addAllহয় সব, নয় কিছুই না — একটি ফাইলও না এলে install ব্যর্থ হয়, যাতে অর্ধেক জমানো অ্যাপ চালু না হয়ে যায়। - Activate — ঘর গোছানো। এখন পুরোনো কোনো service worker কোনো পাতা চালাচ্ছে না, তাই আগের সংস্করণের cache মুছে ফেলার সঠিক সময় এটিই। Install-এ মুছলে তখনো চলতে থাকা পুরোনো পাতা হঠাৎ তার ফাইল হারাত।
- যে পাতা নিবন্ধন করল, সে নিয়ন্ত্রণের বাইরে। নিয়ম হলো: একটি পাতা যে service worker নিয়ে লোড হয়েছে (বা ছাড়া), পুরো জীবন সেভাবেই থাকে। তাই প্রথম ভিজিটে
controllerহলোnull. Activate-এself.clients.claim()ডাকলে খোলা পাতাগুলোর নিয়ন্ত্রণ সাথে সাথে নেওয়া যায় — অনেক অ্যাপ তা-ই করে। - পরের লোড — fetch event. রিফ্রেশ বা পরের ভিজিট থেকে এই scope-এর প্রতিটি অনুরোধ — পাতা, CSS, ছবি,
fetch()— service worker-এরfetchevent হয়ে আসে।event.respondWith()না ডাকলে অনুরোধটি স্বাভাবিকভাবে network-এ যায়।
৪ · আপডেটের সমস্যা — waiting, skipWaiting আর “নতুন সংস্করণ আছে”
আপনি sw.js-এ একটি বাগ ঠিক করে নতুন সংস্করণ সার্ভারে তুললেন। ব্যবহারকারী পাতা রিফ্রেশ করলেন — অথচ পুরোনো আচরণই দেখছেন! কারণ: ব্রাউজার প্রতিটি পাতা খোলার সময় (আর অন্তত ২৪ ঘণ্টায় একবার) sw.js আবার নামিয়ে byte ধরে মিলিয়ে দেখে। এক byte-ও আলাদা হলে নতুন সংস্করণ install হয় — কিন্তু সাথে সাথে চালু হয় না। কেন, ধাপে ধাপে দেখুন:
1সোমবার: v2 চলছে, খাতার ৩টি ট্যাব খোলা2মঙ্গলবার: v3 সার্ভারে, একটি ট্যাব রিফ্রেশ3v3-এর install — নতুন cache তৈরি4v3 waiting — v2 এখনো সব ট্যাব চালায়5সব ট্যাব বন্ধ, বা skipWaiting → v3 active
| active | |
| installing / waiting | |
| cache | |
| খোলা ট্যাব |
- স্থির অবস্থা। v2 active, তিনটি ট্যাব v2-এর নিয়ন্ত্রণে, cache-এ
khata-shell-v2. সব ট্যাবে একই সংস্করণ — সামঞ্জস্য আছে। - নতুন ফাইল পাওয়া গেল। রিফ্রেশের সময় ব্রাউজার
sw.jsনামিয়ে দেখল এক byte হলেও বদলেছে। (এই যাচাইয়ের সময় ব্রাউজার সাধারণত HTTP cache এড়িয়ে যায়, তবু সার্ভারেsw.js-কেCache-Control: no-cacheদেওয়া ভালো।) মজার ব্যাপার: রিফ্রেশ করা পাতাটি তবু v2 দিয়েই লোড হলো। - v3-এর install, v2-এর পাশাপাশি। v3 নিজের নামে নতুন cache (
khata-shell-v3) বানায়। v2-এর cache এখনো অক্ষত — নামের মধ্যে সংস্করণ রাখার এটাই সুবিধা। একই নামে লিখলে v2-এর চলমান পাতাগুলো হঠাৎ v3-এর ফাইল পেত। - Waiting — ইচ্ছাকৃত অপেক্ষা। একটি origin-এ একসাথে একটিই active service worker. v3 এখনই দায়িত্ব নিলে পুরোনো v2-এর HTML আর JavaScript চলমান ট্যাবে v3-এর নতুন নিয়ম আর নতুন ফাইল পেত — দুই সংস্করণের মিশ্রণে অদ্ভুত বাগ। তাই ব্রাউজার অপেক্ষা করে যতক্ষণ v2-এর একটিও ট্যাব খোলা। শুধু রিফ্রেশে ট্যাব “বন্ধ” ধরা হয় না। ভদ্র সমাধান: পাতায় একটি ব্যানার — “নতুন সংস্করণ আছে — রিফ্রেশ করুন”.
- Activate. সব ট্যাব বন্ধ হলে, অথবা v3 নিজে
self.skipWaiting()ডাকলে, v3 active হয়, activate-এshell-v2মুছে দেয়।skipWaitingঅন্ধভাবে install-এই ডাকলে খোলা ট্যাবগুলো পুরোনো পাতা নিয়ে নতুন service worker-এর হাতে পড়ে — ঠিক যে মিশ্রণ এড়াতে waiting বানানো। তাই ব্যবহারকারী ব্যানারে চাপ দিলে তবেই skipWaiting, তারপর পাতা রিলোড।
এই “ব্যবহারকারী চাপলে তবেই আপডেট” প্যাটার্নটি কোডে এমন — পাতার দিকে নতুন সংস্করণ খোঁজা আর ব্যানার দেখানো, service worker-এর দিকে বার্তা পেলে skipWaiting:
// ---- পাতায় ----
const reg = await navigator.serviceWorker.register("/sw.js");
const offerUpdate = (worker) => {
updateBar.hidden = false; // "নতুন সংস্করণ আছে — রিফ্রেশ করুন"
updateBar.querySelector("button").onclick = () => worker.postMessage({ type: "SKIP_WAITING" });
};
if (reg.waiting && navigator.serviceWorker.controller) offerUpdate(reg.waiting);
reg.addEventListener("updatefound", () => {
const nw = reg.installing;
nw.addEventListener("statechange", () => {
// controller থাকলে এটি আপডেট; না থাকলে প্রথম install — ব্যানার নয়
if (nw.state === "installed" && navigator.serviceWorker.controller) offerUpdate(nw);
});
});
let reloading = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (!reloading) { reloading = true; location.reload(); } // নতুন SW এল → নতুন পাতা
});
// ---- sw.js-এ ----
self.addEventListener("message", (event) => {
if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
});৫ · Cache API আর fetch event — অনুরোধ ধরে উত্তর বানানো
Service worker-এর জমানো ফাইল থাকে Cache API-তে — ব্রাউজারের ভেতরের একটি সংরক্ষণাগার, যেখানে চাবি হলো একটি Request (মূলত URL) আর মান একটি Response. এটি ব্রাউজারের সাধারণ HTTP cache থেকে আলাদা: HTTP cache চলে সার্ভারের Cache-Control header দেখে, নিজের নিয়মে, যখন খুশি মুছে দেয়; Cache API পুরোপুরি আপনার নিয়ন্ত্রণে — আপনি না মুছলে (বা জায়গার অভাবে ব্রাউজার পুরো সাইটের storage না মুছলে) থেকে যায়। প্রধান method-গুলো সবই Promise ফেরত দেয়:
| Method | কাজ |
|---|---|
caches.open(name) | নাম দিয়ে একটি cache খোলে (না থাকলে বানায়) |
cache.addAll(urls) | URL-গুলো network থেকে এনে জমায় — সব, নয় কিছুই না |
cache.put(request, response) | আপনার হাতে থাকা একটি উত্তর জমায় |
cache.match(request) / caches.match(request) | জমানো উত্তর খোঁজে; না পেলে undefined (reject নয়!) |
caches.keys() / caches.delete(name) | সব cache-এর নাম / একটি পুরো cache মুছে ফেলা |
পর্ব ৩৫-এর একটি নিয়ম এখানে খুব জরুরি: Response-এর body পড়া যায় একবারই। Network থেকে উত্তর এলে আপনাকে দুটি কাজ করতে হয় — পাতাকে দেওয়া আর cache-এ রাখা। দুটোই body পড়ে। তাই একটি কপি বানাতে হয় response.clone() দিয়ে, body ছোঁয়ার আগেই:
async function cacheFirst(request) {
const hit = await caches.match(request);
if (hit) return hit; // জমা আছে → network-এ যাই না
const res = await fetch(request);
if (res.ok) (await caches.open(RUNTIME)).put(request, res.clone()); // কপি cache-এ
return res; // আসলটি পাতায়
}
async function networkFirst(request, fallbackUrl) {
try {
const res = await fetch(request);
if (res.ok) (await caches.open(RUNTIME)).put(request, res.clone());
return res;
} catch (err) { // network error = অফলাইন
const hit = await caches.match(request);
if (hit) return hit;
if (fallbackUrl) return caches.match(fallbackUrl); // যেমন /offline.html
throw err;
}
}এবার এই function-গুলোকে fetch event-এ জোড়া দিন। একটি service worker-এ সাধারণত একটিই fetch listener থাকে, যা অনুরোধের ধরন দেখে রাস্তা ঠিক করে দেয় — পোস্ট অফিসের বাছাই-টেবিলের মতো:
self.addEventListener("fetch", (event) => {
const req = event.request;
const url = new URL(req.url);
if (req.method !== "GET") return; // POST/PUT/DELETE: ছুঁই না → সরাসরি network
if (url.origin !== location.origin) return; // অন্য সাইটের ফাইল: ব্রাউজারের হাতে
if (url.pathname.startsWith("/api/pay")) return; // পেমেন্ট: network-only
if (req.mode === "navigate") // পুরো পাতা খোলা
return event.respondWith(networkFirst(req, "/offline.html"));
if (url.pathname.startsWith("/api/products"))
return event.respondWith(staleWhileRevalidate(event)); // অংশ ৬
if (url.pathname.startsWith("/api/")) return event.respondWith(networkFirst(req));
event.respondWith(cacheFirst(req)); // বাকি সব: hashed static ফাইল
});return করে চলে যাওয়া (respondWith না ডাকা) মানে “আমি এতে নেই” — অনুরোধটি এমনভাবে যায় যেন service worker নেই। এবার একটি অনুরোধের চারটি সম্ভাব্য পথ পাশাপাশি দেখুন — দুটি strategy, অনলাইন আর অফলাইন অবস্থায়:
- পাতা
fetch(url)অনুরোধ - SW
respondWith()strategy - Cache
match()ফোনে - Network
fetch()সার্ভারে
SW → cache.match() → পাওয়া গেল → পাতায় (১–৫ ms, ০ byte ডেটা খরচ) network-এ অনুরোধ যায়ইনি — অনলাইন থাকলেও cache-এ না থাকলে: network → clone() cache-এ → পাতায়
SW → cache.match() → পাওয়া গেল → পাতায় অফলাইন কি না, টেরই পাওয়া গেল না — app shell এভাবেই খোলে cache-এ না থাকলে: fetch() → TypeError → পাতায় network error
SW → fetch() → 200 OK → res.clone() cache-এ → আসলটি পাতায় ব্যবহারকারী সবসময় সর্বশেষ খবর পান cache-এ এখন একটি ব্যাকআপ কপি রইল
SW → fetch() → TypeError (Failed to fetch) → catch → cache.match() → গতবারের কপি পাতায় পাতা দেখাবে: “অফলাইন — সর্বশেষ আপডেট গতকাল রাত ৯টা” কপিও না থাকলে (পুরো পাতা হলে) → /offline.html
- Cache-first, অনলাইন। আগে cache দেখা হয়, পাওয়া গেলেই উত্তর — network-এ যাওয়াই হয় না। তাই সবচেয়ে দ্রুত আর ডেটা খরচ শূন্য। ঝুঁকি: ফাইল সার্ভারে বদলালেও পুরোনোটিই আসতে থাকবে। এজন্য cache-first শুধু এমন ফাইলে, যার নামে বিষয়বস্তুর hash আছে (
app.3f9a1c.css) — বিষয়বস্তু বদলালে নামই বদলায়। - Cache-first, অফলাইন। একই পথ, একই ফল — নেটওয়ার্কের অবস্থা কোনো ব্যাপারই না। কিন্তু যা আগে কখনো জমা হয়নি, তার জন্য network ছাড়া উপায় নেই — তখন network error.
- Network-first, অনলাইন। আগে network; সফল হলে উত্তরের একটি কপি (
clone()) cache-এ রেখে আসলটি পাতায়। ব্যবহারকারী সর্বশেষ ডেটা পান। দুর্বলতা: খুব ধীর নেটওয়ার্কে দীর্ঘ অপেক্ষা — তাই সাথে কয়েক সেকেন্ডের timeout দিন (পর্ব ৩৬-এরAbortSignal.timeout). - Network-first, অফলাইন।
fetchTypeError দিয়ে reject হয় (পর্ব ৩৫-এর network error), catch-এ cache থেকে গতবারের কপি। পাতাকে জানানো ভালো যে ডেটা পুরোনো। পুরো পাতার অনুরোধে কিছুই না পেলে বাংলায় লেখা offline fallback পাতা।
অফলাইন পাতাটি নিজেও precache করা থাকতে হবে, আর এর ভেতরের সব কিছু (CSS, ছবি) হয় precache করা, নয়তো পাতার ভেতরেই লেখা:
<!doctype html>
<html lang="bn">
<meta charset="utf-8"><meta name="viewport" content="width=device-width">
<title>অফলাইন — খাতা</title>
<style>body{font-family:sans-serif;text-align:center;padding:3rem 1rem}</style>
<h1>আপনি এখন অফলাইনে</h1>
<p>এই পাতাটি আগে খোলা হয়নি, তাই ফোনে জমা নেই।</p>
<p>আপনার খাতার আগের হিসাব আর নতুন খরচ যোগ করা অফলাইনেও চলবে।</p>
<p><a href="/">খাতায় ফিরে যান</a> · <button onclick="location.reload()">আবার চেষ্টা করুন</button></p>যা কখনো cache করবেন না। (১) POST-এর মতো অনুরোধ — Cache API নিজেই cache.put-এ GET ছাড়া অন্য method নিলে TypeError দেয়, আর অর্থের দিক থেকেও “অর্ডার দাও”-এর উত্তর জমিয়ে আবার দেওয়া অর্থহীন। (২) লগইন-করা ব্যবহারকারীর ব্যক্তিগত উত্তর — প্রোফাইল, অর্ডারের ইতিহাস, token-যুক্ত উত্তর। একই ফোনে কেউ লগআউট করে অন্যজন লগইন করলে আগের জনের তথ্য cache থেকে বেরিয়ে আসতে পারে; করতেই হলে লগআউটের সময় সেই cache মুছুন। (৩) পেমেন্ট, OTP আর দাম নিশ্চিত করার মতো যেকোনো কিছু — এগুলো সবসময় সরাসরি network. (৪) Error উত্তর — res.ok না দেখে জমালে একটি 500 পাতা বারবার cache থেকে আসবে।
৬ · Caching strategy — কোনটি কখন
Service worker-এর আসল দক্ষতা কোড লেখা নয়, প্রতিটি ধরনের অনুরোধের জন্য সঠিক নিয়ম বাছাই। পাঁচটি প্রচলিত caching strategy:
| Strategy | কীভাবে | কোথায় ব্যবহার | ঝুঁকি |
|---|---|---|---|
| Cache-first | cache-এ থাকলে সেটিই; না থাকলে network, তারপর জমানো | নামে hash-যুক্ত CSS/JS, font, আইকন | নাম না বদলালে পুরোনো ফাইল আটকে থাকে |
| Network-first | আগে network; ব্যর্থ হলে cache | খবর, অ্যাকাউন্টের ব্যালান্স দেখানো, HTML পাতা | ধীর নেটওয়ার্কে অপেক্ষা — timeout দিন |
| Stale-while-revalidate | cache থেকে সাথে সাথে, পেছনে network থেকে নতুন এনে cache হালনাগাদ | পণ্যের তালিকা, অবতার ছবি, কম জরুরি API | ব্যবহারকারী একবার পুরোনো ডেটা দেখেন |
| Network-only | শুধু network, cache-এ কিছুই না | পেমেন্ট, লগইন, OTP, analytics | অফলাইনে কাজ করে না — সেটাই উদ্দেশ্য |
| Cache-only | শুধু cache | install-এ precache করা app shell | precache-এ না থাকলে কখনো আসবে না |
তৃতীয়টির নামের অর্থ: “stale” (বাসি) কপি এখনই দাও, “while revalidate” — একই সময়ে পেছনে নতুন কপি যাচাই করে আনো। পণ্যের তালিকার ক্ষেত্রে এটি চমৎকার: ক্রেতা অ্যাপ খুলেই তালিকা দেখেন, আর দাম এক-আধ ঘণ্টা পুরোনো হলেও ক্ষতি নেই, কারণ অর্ডারের সময় দাম ঠিক করবে সার্ভার। ধাপে ধাপে দেখুন:
1const hit = await cache.match(req);2const update = fetch(req).then(res => { cache.put(req, res.clone()); return res; });3event.waitUntil(update.catch(() => {}));4return hit ?? update; // পাতা পায় hit5// ... পরের বার অ্যাপ খুললে: cache.match → নতুন দাম
| cache-এ চালের দাম | |
| network অনুরোধ | |
| পর্দায় |
- cache দেখা। প্রথমে জমানো উত্তর খোঁজা — গতকালের তালিকা, চাল ৭০৳.
- পেছনে network শুরু।
fetchশুরু করা হলো, কিন্তুawaitকরা হয়নি — Promise-টিupdateভ্যারিয়েবলে রাখা হলো। উত্তর এলে তার একটি কপি cache-এ যাবে (clone(), কারণ body একবারই পড়া যায়)। - Service worker-কে জাগিয়ে রাখা। উত্তর পাতায় দেওয়ার পর ব্রাউজার service worker থামিয়ে দিতে পারে — তখন পেছনের কাজটি অর্ধেকে মারা যেত।
event.waitUntil(update)বলে “এটি শেষ হওয়া পর্যন্ত থেমো না।” আর অফলাইন হলেupdatereject হবে —.catch(() => {})সেটিকে চুপ করায়, যাতে unhandled rejection না হয়। - পাতা পায় পুরোনোটা — সাথে সাথে।
hitথাকলে সেটিই উত্তর; পর্দায় এখনই ৭০৳. প্রথমবার, যখন cache-এ কিছু নেই,hit ?? updatenetwork-এর উত্তরের জন্যই অপেক্ষা করে। পেছনে ইতিমধ্যে cache ৭২৳-এ হালনাগাদ হয়ে গেছে। - পরের বার নতুনটা। পরের বার তালিকা খুললে cache থেকেই ৭২৳ আসে, আর পেছনে আবার যাচাই চলে। চাইলে হালনাগাদ শেষে
postMessageদিয়ে পাতাকে জানানো যায়।
পুরো ধারণাটি চালিয়ে দেখুন। আসল Cache API আর service worker এই কোড-বক্সে চলে না (অংশ ২), তাই নিচের কোডে cache হলো একটি সাধারণ Map, আর network একটি নকল async function, যাকে ইচ্ছামতো “অফলাইন” করা যায়। Strategy-র যুক্তি হুবহু একই:
const cache = new Map([["/app.css", "app.css v1"], ["/api/products", "চাল ৭০৳ (গতকাল)"]]);
let online = true;
const netGet = async (url) => { if (!online) throw new TypeError("Failed to fetch"); // নকল network
return url === "/api/products" ? "চাল ৭২৳ (আজ)" : `${url} (নেট থেকে)`; };
const fetchAndStore = async (url) => { const body = await netGet(url); cache.set(url, body); return body; };
const cacheFirst = async (url) =>
cache.has(url) ? `${cache.get(url)} ← cache` : `${await fetchAndStore(url)} ← network`;
const networkFirst = (url) => fetchAndStore(url).then((body) => `${body} ← network`,
() => `${cache.get(url) ?? "offline.html"} ← cache (অফলাইন)`); // ব্যর্থ হলে cache
function staleWhileRevalidate(url) {
const update = fetchAndStore(url).catch(() => {}); // পেছনে চলবে, অপেক্ষা নয়
return { now: `${cache.get(url) ?? "(খালি)"} ← cache`, update };
}
(async () => {
console.log(await cacheFirst("/app.css"), "|", await cacheFirst("/logo.png"));
const swr = staleWhileRevalidate("/api/products"); console.log("এখনই:", swr.now);
await swr.update; console.log("পরের বারের জন্য cache-এ:", cache.get("/api/products"));
online = false; console.log(await networkFirst("/api/products"), "|", await networkFirst("/news"));
try { await caches.open("v1"); console.log("আসল Cache API চলছে"); }
catch (e) { console.log("আসল Cache API এখানে বন্ধ:", e.name); }
})();প্রথম লাইন app.css v1 ← cache | /logo.png (নেট থেকে) ← network — cache-first: জমানো ফাইল সাথে সাথে, না-জমানোটি network থেকে এনে জমানো হলো। তারপর stale-while-revalidate: এখনই: চাল ৭০৳ (গতকাল) ← cache — পর্দায় অপেক্ষা ছাড়াই পুরোনো দাম; পেছনের কাজ শেষ হলে পরের বারের জন্য cache-এ: চাল ৭২৳ (আজ). এরপর নেটওয়ার্ক বন্ধ: চাল ৭২৳ (আজ) ← cache (অফলাইন) | offline.html ← cache (অফলাইন) — network-first ব্যর্থ হয়ে cache-এ ফিরল; আর কখনো না-দেখা /news-এর জন্য অফলাইন পাতা। শেষ লাইনটি feature detection: আসল caches.open চেষ্টা করতেই Edge/Chrome দিল SecurityError — opaque origin-এ ব্রাউজার কোনো স্থায়ী storage দেয় না, তাই চেষ্টাটি try/catch-এ মোড়ানো। (আসল পাতায় caches শুধু service worker-এ নয়, সাধারণ পাতার JavaScript থেকেও ব্যবহার করা যায় — যেমন “এই প্রবন্ধটি অফলাইনে পড়ার জন্য সেভ করুন” বোতাম।)
৭ · অফলাইন ডেটা — IndexedDB, Background Sync আর Push
Cache API ফাইলের জন্য, IndexedDB ডেটার জন্য
অফলাইনে লেখা ১৫টি খরচ কোথায় থাকবে? Cache API-তে থাকে Request→Response জোড়া — ফাইল আর API উত্তরের জন্য উপযুক্ত, কিন্তু ব্যবহারকারীর নিজের তৈরি record খোঁজা, বদলানো বা গোনার জন্য নয়। সেই কাজের জন্য ব্রাউজারের ডেটাবেস IndexedDB (পর্ব ৪০), যা পাতা আর service worker দুই জায়গা থেকেই ব্যবহার করা যায়। localStorage service worker-এ নেই, কারণ সেটি synchronous. এই নকশাকে বলে offline-first: অ্যাপ সবসময় আগে স্থানীয় ডেটাবেসে লেখে আর সেখান থেকে পড়ে, সার্ভারের সাথে মেলানো হয় পরে, সুযোগমতো।
জায়গা কিন্তু অসীম নয়। প্রতিটি origin-এর একটি storage quota আছে — ব্রাউজারভেদে আলাদা, সাধারণত ডিস্কের খালি জায়গার একটি অংশ। await navigator.storage.estimate() জানায় কত ব্যবহার হয়েছে আর কত পাওয়া যাবে। জায়গা কমলে ব্রাউজার কম ব্যবহৃত সাইটের পুরো storage মুছে দিতে পারে (navigator.storage.persist() দিয়ে না মোছার অনুরোধ করা যায়); Safari কিছুদিন ব্যবহার না হওয়া সাধারণ সাইটের storage মুছে ফেলে, হোম স্ক্রিনের অ্যাপ বাদে। তাই runtime cache-এ সীমা রাখুন (যেমন সর্বোচ্চ ৫০টি ছবি), আর গুরুত্বপূর্ণ ডেটা দ্রুত সার্ভারে পাঠান। বিস্তারিত পর্ব ৪০-এ।
অফলাইনে অর্ডার, অনলাইনে পাঠানো — Background Sync
পর্ব ৩৬-এ একটি প্রশ্ন রেখে আসা হয়েছিল: নেটওয়ার্কহীন জায়গায় একজন মাঠকর্মী ফর্ম জমা দিলেন — পাতা বন্ধ করে ফেললেও কীভাবে পরে পাঠানো যাবে? উত্তর Background Sync: পাতা কাজটি IndexedDB-র একটি “outbox”-এ রেখে service worker-কে বলে “সংযোগ পেলে আমাকে ডেকো।” সংযোগ ফিরলেই ব্রাউজার service worker-কে sync event দেয় — পাতা খোলা না থাকলেও। পাঠাতে ব্যর্থ হলে (Promise reject) ব্রাউজার পরে আবার চেষ্টা করে।
// ---- পাতায়: "অর্ডার দিন" চাপলে ----
await outbox.add({ id: crypto.randomUUID(), items, createdAt: Date.now() }); // IndexedDB (পর্ব ৪০)
showToast("অর্ডার জমা আছে — সংযোগ পেলে পাঠানো হবে");
const reg = await navigator.serviceWorker.ready;
if ("sync" in reg) {
await reg.sync.register("send-orders"); // Chromium-এ
} else {
window.addEventListener("online", sendOutbox); // অন্য ব্রাউজারে: পাতা খোলা থাকলে
if (navigator.onLine) sendOutbox();
}
// ---- sw.js ----
self.addEventListener("sync", (event) => {
if (event.tag === "send-orders") event.waitUntil(sendOutbox());
});
async function sendOutbox() {
for (const order of await outbox.getAll()) {
const res = await fetch("/api/orders", { method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": order.id },
body: JSON.stringify(order) });
if (!res.ok) throw new Error(`HTTP ${res.status}`); // reject → ব্রাউজার পরে আবার চেষ্টা করবে
await outbox.delete(order.id);
}
}তিনটি সতর্কতা। এক, Background Sync এখন পর্যন্ত শুধু Chromium-ভিত্তিক ব্রাউজারে (Chrome, Edge, Samsung Internet, Opera) আছে; Firefox আর Safari-তে নেই — তাই "sync" in reg দেখে online event-এর বিকল্প পথ রাখা বাধ্যতামূলক। দুই, প্রতিটি অর্ডারের সাথে Idempotency-Key — sync বারবার চলতে পারে, আর একই অর্ডার দুবার যাওয়া চলবে না (পর্ব ৩৫-এর idempotent ধারণা)। তিন, navigator.onLine সত্য মানেই ইন্টারনেট আছে, এমন নয় — প্রমাণ শুধু সফল fetch.
Push notification — পাতা বন্ধ, তবু খবর
Service worker-এর আরেকটি ক্ষমতা হলো সার্ভার থেকে বার্তা পাওয়া, যখন সাইটের কোনো ট্যাব খোলা নেই। এর জন্য দুটি API একসাথে কাজ করে: Push API বার্তা পৌঁছায়, আর Notification API পর্দায় দেখায় (Notification-এর মূল কথা পর্ব ৪১-এ)। পথটি এমন: ব্যবহারকারী অনুমতি দিলে ব্রাউজার তার নিজের push service-এর (Chrome-এ Google-এর, Firefox-এ Mozilla-র, Safari-তে Apple-এর) একটি ঠিকানা দেয় — subscription. আপনি সেটি নিজের সার্ভারে রাখেন। পরে সার্ভার সেই ঠিকানায় এনক্রিপ্ট করা বার্তা পাঠায়, push service সেটি ফোনে পৌঁছে দেয়, আর ব্রাউজার আপনার service worker-কে জাগিয়ে push event দেয়। আপনার সার্ভার যে আসল প্রেরক, তা প্রমাণ করতে একজোড়া key লাগে — VAPID key; public key যায় subscribe-এর সময়, private key থাকে শুধু সার্ভারে।
// ---- পাতায়: "অর্ডারের খবর পেতে চাই" বোতামে চাপ দেওয়ার পর — পাতা খোলামাত্র নয় ----
if (await Notification.requestPermission() === "granted") {
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true, // প্রতিটি push-এ দৃশ্যমান notification দেখাতে হবে
applicationServerKey: VAPID_PUBLIC_KEY, // public key — গোপন নয়
});
await fetch("/api/push/subscribe", { method: "POST",
headers: { "Content-Type": "application/json" }, body: JSON.stringify(sub) });
}
// ---- sw.js ----
self.addEventListener("push", (event) => {
const msg = event.data?.json() ?? { title: "অফলাইন খাতা", body: "নতুন খবর আছে" };
event.waitUntil(self.registration.showNotification(msg.title, { body: msg.body, icon: "/icons/192.png" }));
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(clients.openWindow("/orders"));
});সমর্থনের অবস্থা সাবধানে বলা দরকার। Chrome, Edge আর Firefox ডেস্কটপ ও Android-এ বহু বছর ধরে web push দেয়। Safari macOS-এ (Ventura থেকে) standard web push দেয়, আর iPhone/iPad-এ iOS/iPadOS 16.4 (২০২৩) থেকে — কিন্তু শুধু তখন, যখন ব্যবহারকারী সাইটটি হোম স্ক্রিনে যোগ করেছেন; সাধারণ Safari ট্যাবে নয়। আর ব্যবহারিক নিয়ম: অনুমতি চাইবেন কোনো অর্থবহ মুহূর্তে, ব্যবহারকারীর কাজের পর (“অর্ডার দিয়েছেন — ডেলিভারির খবর পেতে চান?”), পাতা খোলামাত্র নয়। একবার “Block” চাপলে আর জিজ্ঞেস করার সুযোগ থাকে না।
৮ · বাস্তবে — Workbox, DevTools, নিরাপত্তা আর PWA বনাম native অ্যাপ
নিজের কম্পিউটারে চালিয়ে দেখুন
উপরের sw.js, manifest.webmanifest, offline.html আর একটি সাধারণ index.html এক ফোল্ডারে রাখুন। ফাইলটি সরাসরি ডাবল-ক্লিক করে খুললে (file://) service worker চলবে না — একটি ছোট সার্ভার লাগবে:
cd khata-pwa
npx serve . # Node থাকলে — ঠিকানা দেখাবে, সাধারণত http://localhost:3000
# অথবা Python থাকলে:
python -m http.server 8000 # তারপর http://localhost:8000এবার DevTools (F12) → Application প্যানেল খুলুন — PWA-র নিয়ন্ত্রণকক্ষ:
- Manifest: ব্রাউজার manifest কীভাবে পড়ল, আইকনগুলো, আর ইনস্টলযোগ্যতার সমস্যা থাকলে সতর্কবার্তা।
- Service workers: কোন সংস্করণ active, কোনটি waiting (পাশে “skipWaiting” লিংক)। Update on reload চালু রাখলে ডেভেলপমেন্টের সময় প্রতিটি রিফ্রেশে নতুন service worker সাথে সাথে চালু হয় — waiting-এর ঝামেলা ছাড়া। Offline চেকবক্স দিয়ে নেটওয়ার্ক বন্ধ করে দেখুন অ্যাপ টিকে থাকে কি না। Push আর Sync বোতাম দিয়ে event নকল করা যায়।
- Cache storage: প্রতিটি cache-এর নাম আর ভেতরের প্রতিটি উত্তর —
khata-shell-v3-এ সত্যিই সব ফাইল আছে কি না। - Storage → Clear site data: সব cache, IndexedDB আর service worker একসাথে মুছে “প্রথম ভিজিট” থেকে আবার শুরু।
Network tab-এ cache থেকে আসা উত্তরের পাশে লেখা থাকে “(ServiceWorker)”. আগে Lighthouse-এ একটি আলাদা “PWA” মূল্যায়ন ছিল; ২০২৪ সালে Lighthouse 12-এ সেটি সরিয়ে দেওয়া হয়েছে, তাই পুরোনো tutorial-এর “PWA score ১০০” লক্ষ্য এখন আর প্রযোজ্য নয়। ইনস্টলযোগ্যতা দেখুন Application → Manifest-এ, আর গতির জন্য Lighthouse-এর Performance অংশ।
Workbox — হাতে লেখার বদলে পরীক্ষিত library
এই পর্বের হাতে লেখা কোড ধারণা বোঝার জন্য। বাস্তব প্রজেক্টে precache-এর তালিকা বানানো (প্রতিটি build-এ ফাইলের নাম বদলায়!), cache-এর আকার সীমিত রাখা, পুরোনো cache মোছা, Background Sync না থাকলে বিকল্প — এসব বারবার নির্ভুল লেখা কঠিন। Google-এর Workbox library এগুলো তৈরি অবস্থায় দেয়, আর Vite-এর vite-plugin-pwa-এর মতো build টুল (পর্ব ৪৬) এর ওপর ভিত্তি করেই চলে:
import { precacheAndRoute } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { NetworkFirst, StaleWhileRevalidate, CacheFirst } from "workbox-strategies";
import { ExpirationPlugin } from "workbox-expiration";
precacheAndRoute(self.__WB_MANIFEST); // build টুল এখানে hash-সহ ফাইলের তালিকা বসায়
registerRoute(({ request }) => request.mode === "navigate", new NetworkFirst({ cacheName: "pages" }));
registerRoute(({ url }) => url.pathname.startsWith("/api/products"),
new StaleWhileRevalidate({ cacheName: "products" }));
registerRoute(({ request }) => request.destination === "image",
new CacheFirst({ cacheName: "images", plugins: [new ExpirationPlugin({ maxEntries: 50 })] }));নিরাপত্তা — একটি খারাপ service worker পুরো সাইট আটকে দিতে পারে
Service worker ব্যবহারকারীর ফোনে থেকে যায় আর প্রতিটি অনুরোধ নিয়ন্ত্রণ করে। ভুল কোড — যেমন HTML-কে cache-first করে দেওয়া — মানে আপনি সার্ভারে যত নতুন সংস্করণই তুলুন, ব্যবহারকারী পুরোনো পাতাই দেখবেন, কারণ নতুন পাতা চাওয়ার অনুরোধ সার্ভারে পৌঁছাচ্ছেই না। সৌভাগ্যক্রমে sw.js-এর আপডেট-যাচাই সবসময় সার্ভারে যায়, তাই উদ্ধারের পথ আছে — একটি kill switch service worker:
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
for (const name of await caches.keys()) await caches.delete(name); // সব cache মোছা
await self.registration.unregister(); // নিজেকে সরিয়ে নেওয়া
for (const client of await self.clients.matchAll({ type: "window" }))
client.navigate(client.url); // খোলা পাতাগুলো আবার লোড
})());
});একই ঠিকানায় (/sw.js) এটি তুললে ব্যবহারকারীর পরের ভিজিটে পুরোনো service worker সরে যায়। সার্ভার থেকে Clear-Site-Data: "storage" header পাঠানোও একটি বিকল্প। আরও নিয়ম: service worker ফাইলের ঠিকানা কখনো বদলাবেন না (sw-v2.js দিলে পুরোনো sw.js কারও ফোনে থেকে যেতে পারে); scope যতটুকু দরকার ততটুকু রাখুন; আর পর্ব ৪৩-এর XSS থেকে সাইট রক্ষা করুন — XSS দিয়ে আক্রমণকারী service worker নিবন্ধন করাতে পারলে ক্ষতি সাময়িক থাকে না।
PWA, native অ্যাপ আর Play Store
| বিষয় | PWA | Native অ্যাপ (Kotlin/Java, Swift) |
|---|---|---|
| বিতরণ | একটি লিংক; store-এর অনুমোদন লাগে না; আপডেট সাথে সাথে | Play Store/App Store; রিভিউ, ব্যবহারকারীকে আপডেট নামাতে হয় |
| এক কোডে কয়টি প্ল্যাটফর্ম | Android, iPhone, Windows, Mac, Linux — একই কোড | প্রতিটির জন্য আলাদা (বা cross-platform framework) |
| যন্ত্রের ক্ষমতা | ক্যামেরা, অবস্থান, notification আছে; Bluetooth, পটভূমিতে দীর্ঘ কাজ ইত্যাদি সীমিত বা ব্রাউজারভেদে আলাদা | প্রায় সব |
মাঝামাঝি একটি পথও আছে: Trusted Web Activity (TWA) দিয়ে একটি PWA-কে Android অ্যাপ হিসেবে মুড়ে Play Store-এ দেওয়া যায় — ভেতরে আসলে Chrome-এ আপনার সাইটই চলে, ঠিকানার বার ছাড়া। Bubblewrap-এর মতো টুল এই কাজ সহজ করে। বাস্তব উদাহরণ হিসেবে প্রায়ই নাম আসে Twitter Lite (২০১৭, এখন X-এর মোবাইল ওয়েব), Starbucks-এর অর্ডারের PWA আর ভারতের Flipkart Lite-এর (২০১৫) — এদের লক্ষ্য ছিল ঠিক আমাদের মতো ধীর নেটওয়ার্ক আর কম দামি ফোনের ব্যবহারকারী। প্রকাশিত বিভিন্ন “এত শতাংশ বেড়েছে” সংখ্যা কোম্পানির নিজস্ব দাবি; সংখ্যার চেয়ে নকশার শিক্ষাটি বেশি কাজের।
Java বা Kotlin-এ native Android অ্যাপে একই সমস্যার সমাধান আলাদা যন্ত্রে হয়: অফলাইন ডেটা Room/SQLite-এ (IndexedDB-র কাজ), আর “সংযোগ ফিরলে পাঠাও” WorkManager-এ (Background Sync-এর কাজ); নেটওয়ার্কের মূল কথা Java Networking পর্বে। Service worker-এর ধারণাটি সার্ভার-জগতের Squid বা nginx-এর proxy_cache-এর মতো caching proxy থেকে আসা — তফাত হলো, ওগুলো নেটওয়ার্কের মাঝখানে অনেক ব্যবহারকারীর জন্য বসে, আর service worker বসে প্রতিটি ব্যবহারকারীর ব্রাউজারের ভেতরে, শুধু একটি সাইটের জন্য, আর তার নিয়ম লেখা হয় সাধারণ JavaScript-এ। C, C++ বা Python-এর standard library-তে এর সরাসরি কোনো সমতুল্য নেই।
- Service worker আর Web Worker-এর পার্থক্য? — দুটোই main thread-এর বাইরে চলে, DOM ছোঁয় না; Web Worker একটি পাতার ভারী হিসাবের জন্য, service worker একটি origin-এর নেটওয়ার্ক proxy, পাতা বন্ধ থাকলেও event পায়।
- Service worker-এর জীবনচক্রের ধাপ? — register → install (precache) → waiting (পুরোনো সংস্করণ চললে) → activate (পুরোনো cache মোছা) → fetch/push/sync event.
- Service worker কেন HTTPS চায়? — এটি সব অনুরোধ বদলাতে পারে আর ফোনে থেকে যায়; পথে কেউ ঢুকিয়ে দিলে দীর্ঘস্থায়ী আক্রমণ হতো। ব্যতিক্রম শুধু localhost.
- Cache-first আর network-first কখন? — নামে hash-যুক্ত static ফাইলে cache-first; সর্বশেষ ডেটা জরুরি এমন API বা HTML-এ network-first, অফলাইনে cache-এ ফেরা।
- PWA হতে ন্যূনতম কী লাগে? — HTTPS, name/icons/start_url/display-সহ web app manifest, আর অফলাইনের জন্য একটি service worker.
৯ · AI-এর যুগে — service worker-এ AI-এর চেনা ভুল
“আমার সাইটকে PWA বানিয়ে দাও” — AI এক মিনিটে manifest আর sw.js লিখে দেবে, আর প্রথম দিন সব ঠিকঠাক চলবে। সমস্যা ধরা পড়ে দ্বিতীয় deploy-এ: নতুন সংস্করণ কারও কাছে পৌঁছায় না, বা কেউ অন্যের তথ্য দেখেন। AI-এর লেখা কোডে বারবার দেখা ভুলগুলো:
| AI যা প্রায়ই লেখে | সমস্যা | যা চাইবেন |
|---|---|---|
সব অনুরোধে caches.match(e.request) || fetch(...) আর সব উত্তর cache.put | POST-এ TypeError; লগইন, প্রোফাইল, token-যুক্ত উত্তর জমে যায়; error পাতাও জমে | শুধু GET, শুধু নিজের origin, res.ok, ব্যক্তিগত ও পেমেন্টের পথ বাদ |
caches.open("my-cache") — সংস্করণ নেই, activate-এ মোছা নেই | পুরোনো ফাইল চিরকাল, storage ফুলতে থাকে | নামে সংস্করণ, activate-এ পুরোনো cache মোছা |
| HTML পাতাও cache-first | নতুন deploy কেউ দেখে না | navigation-এ network-first + অফলাইন পাতা |
install-এ অন্ধ self.skipWaiting() | পুরোনো পাতা নতুন service worker-এর হাতে — মিশ্র সংস্করণের বাগ | “নতুন সংস্করণ আছে” ব্যানার, চাপ দিলে skipWaiting + reload |
http:// সাইটে নিবন্ধন, বা file:// থেকে পরীক্ষা | চুপচাপ ব্যর্থ — “কাজ করছে না” বলে বিভ্রান্তি | HTTPS বা localhost; register-এর ভুল ধরা |
| পুরোনো installability তালিকা, Lighthouse PWA score, “Background Sync সব ব্রাউজারে আছে” | সময়ের সাথে বদলে যাওয়া তথ্য | বর্তমান ব্রাউজারের docs আর DevTools-এর Manifest প্যানেল |
AI-কে service worker লিখতে বললে প্রতিটি পথের নিয়ম আগেই স্পষ্ট করে দিন, আর আপডেট ও নিরাপত্তার শর্ত জুড়ে দিন:
AI-এর লেখা service worker — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১AppCache-এর মতো একটি “সহজ” ঘোষণামূলক ব্যবস্থা কেন ব্যর্থ হলো, আর তার জায়গায় কেন এত নিচু স্তরের, প্রোগ্রাম করতে হয় এমন service worker বানানো হলো?
HTML5-এর অংশ হিসেবে আসা Application Cache, সংক্ষেপে AppCache, প্রথম দেখায় চমৎকার ছিল। একটি manifest ফাইলে লিখে দিন কোন ফাইল জমাতে হবে, কোনগুলো সবসময় network থেকে, আর অফলাইনে কোন পাতা দেখাতে হবে — ব্যস, ব্রাউজার বাকিটা করবে। কোনো JavaScript লাগবে না। সমস্যা হলো, “বাকিটা” ব্রাউজার করত নিজের ঠিক করা নিয়মে, আর সেই নিয়মগুলো বাস্তব অ্যাপের সাথে প্রায়ই মিলত না।
২০১২ সালে Jake Archibald A List Apart-এ “Application Cache is a Douchebag” শিরোনামে একটি লেখায় ফাঁদগুলো একে একে দেখান। যে পাতা manifest উল্লেখ করত, সেটি নিজেও সবসময় cache থেকে আসত — তালিকায় না রাখলেও। ফাইল বদলালেও কিছু হতো না, যদি না manifest ফাইলটি নিজে এক byte বদলায়। আর বদলালেও নতুন ফাইল আসত পরের ভিজিটে, কারণ বর্তমান পাতা আগেই পুরোনো ফাইল দিয়ে খুলে গেছে। একটি ফাইলও নামাতে ব্যর্থ হলে পুরো আপডেট বাতিল। আর নিয়ম বদলানোর কোনো hook ডেভেলপারের হাতে ছিল না।
এখান থেকে শিক্ষা নিয়ে Alex Russell, Jake Archibald সহ কয়েকজন ২০১৩–১৪ সালে উল্টো পথে হাঁটেন। নিয়ম ঘোষণা করার বদলে ব্রাউজার দেবে কয়েকটি মৌলিক যন্ত্র: অনুরোধ ধরার একটি event (fetch), উত্তর জমানোর একটি সংরক্ষণাগার (Cache API), আর জীবনচক্রের স্পষ্ট ধাপ (install, activate). নিয়ম লিখবেন ডেভেলপার নিজে, JavaScript-এ। Chrome ২০১৫ সালের শুরুতে service worker চালু করে, Firefox পরের বছর, আর AppCache পরে মানদণ্ড থেকে বাদ পড়ে; Firefox (২০২০) আর Chrome (২০২১) থেকে এটি পুরোপুরি সরিয়ে ফেলা হয়।
মূল উপলব্ধি: উচ্চ স্তরের সহজ ব্যবস্থা যদি নিচের স্তরের ওপর না দাঁড়িয়ে সরাসরি ব্রাউজারে বাঁধা থাকে, তার ভুল নিয়ম কেউ ঠিক করতে পারে না। আগে নমনীয় মৌলিক যন্ত্র, তারপর তার ওপর সহজ library — এই ক্রমই টেকসই।
প্র ০২“Progressive Web App” নামটি কারা, কেন দিয়েছিলেন? একটি প্রযুক্তিকে নাম দেওয়ার কী শক্তি — আর নামটি কোথায় বিভ্রান্তি তৈরি করে?
২০১৫ সালে Google Chrome দলের প্রকৌশলী Alex Russell আর ডিজাইনার-ডেভেলপার Frances Berriman একটি সমস্যা নিয়ে ভাবছিলেন: service worker, manifest, push — নতুন যন্ত্রগুলো তো এসে গেছে, কিন্তু ব্যবসার সিদ্ধান্ত নেওয়া মানুষদের এগুলোর মূল্য বোঝানো যাচ্ছে না। দরকার ছিল এমন একটি নাম, যা ফলাফলটি বোঝায় — এমন ওয়েবসাইট, যা অ্যাপের মতো আচরণ করে। Berriman “Progressive Web Apps” নামটি প্রস্তাব করেন, আর Russell সেই বছরের জুনে নিজের ব্লগে “Progressive Web Apps: Escaping Tabs Without Losing Our Soul” লেখায় ধারণাটি ব্যাখ্যা করেন।
লেখাটিতে বৈশিষ্ট্যের একটি তালিকা ছিল: responsive, সংযোগ-নিরপেক্ষ (অফলাইনে চলে), অ্যাপের মতো অনুভূতি, সবসময় হালনাগাদ, HTTPS-এ নিরাপদ, খুঁজে পাওয়া যায় (manifest দিয়ে চেনা যায়), ফিরে ডাকতে পারে (push), ইনস্টল করা যায়, আর একটি লিংক দিয়েই ভাগ করা যায়। খেয়াল করুন, তালিকার কোনোটিই নতুন একটি প্রযুক্তি নয় — এটি কয়েকটি বিদ্যমান প্রযুক্তির একটি সমন্বয় আর একটি মান। “Progressive” শব্দটি এসেছে progressive enhancement থেকে: পুরোনো ব্রাউজারে সাধারণ ওয়েবসাইট, নতুনটিতে অ্যাপ।
কিন্তু নামের দামও আছে। প্রথমত, অনেকে ধরে নেন PWA একটি আলাদা ধরনের অ্যাপ, একটি framework, বা একটি “হ্যাঁ/না” মানদণ্ড — আসলে এটি একটি বর্ণালি; একটি সাইট কিছুটা বা অনেকটা PWA হতে পারে। দ্বিতীয়ত, “App” শব্দটি এমন প্রত্যাশা তৈরি করে যা প্ল্যাটফর্ম সবসময় পূরণ করে না — বিশেষ করে iPhone-এ। তৃতীয়ত, Lighthouse-এর PWA চেকলিস্ট বহু বছর ধরে নামটিকে একটি স্কোরে পরিণত করেছিল, যা ২০২৪ সালে সরিয়ে নেওয়া হয়। আজকের পরামর্শ তাই নাম নয়, ক্ষমতার দিকে তাকানো: আমার ব্যবহারকারীর কি অফলাইন দরকার, ইনস্টল দরকার, notification দরকার?
মূল উপলব্ধি: প্রযুক্তির নাম একটি যোগাযোগের যন্ত্র — সঠিক নাম সিদ্ধান্তগ্রহীতাদের কাছে মূল্য পৌঁছে দেয়। কিন্তু প্রকৌশলীর কাজ নামের পেছনের আসল ক্ষমতা আর সীমাগুলো আলাদা করে বোঝা।
প্র ০৩Apple কেন PWA-র সুবিধাগুলো দেরিতে দিয়েছে, আর ২০২৪ সালে ইউরোপের DMA আইনের সময় কী ঘটেছিল? একটি প্ল্যাটফর্মের মালিক ওয়েবের ভবিষ্যৎ কতটা নিয়ন্ত্রণ করতে পারে?
মজার বিষয়, “ওয়েব অ্যাপকে হোম স্ক্রিনে রাখা”-র ধারণা প্রথম জোরালোভাবে এনেছিল Apple নিজেই — ২০০৭ সালে প্রথম iPhone-এর সময় Steve Jobs তৃতীয় পক্ষের অ্যাপের পথ হিসেবে ওয়েব অ্যাপের কথাই বলেছিলেন; ২০০৮ সালে App Store আসার পর অগ্রাধিকার বদলে যায়। Chrome ২০১৫ সালে service worker দিলেও Safari দেয় ২০১৮ সালে (iOS 11.3 আর macOS-এ Safari 11.1), প্রায় তিন বছর পর। Web push আসে আরও পরে — macOS Ventura-র Safari-তে ২০২২ সালে, আর iPhone/iPad-এ iOS 16.4-এ ২০২৩ সালে, তাও শুধু হোম স্ক্রিনে যোগ করা অ্যাপে।
আরেকটি গুরুত্বপূর্ণ বাস্তবতা: iPhone-এ App Store-এর নিয়মে বহু বছর ধরে সব ব্রাউজারকে Apple-এর WebKit engine ব্যবহার করতে হয়েছে। ফলে Safari যা দেয় না, iPhone-এ কোনো ব্রাউজারই তা দিতে পারত না। সমালোচকরা বলেন, এতে ওয়েব অ্যাপ ইচ্ছাকৃতভাবে দুর্বল রাখা হয়েছে, যাতে App Store-এর ব্যবসা সুরক্ষিত থাকে; Apple-এর যুক্তি ছিল নিরাপত্তা, গোপনীয়তা আর ব্যাটারি।
২০২৪ সালে ইউরোপীয় ইউনিয়নের Digital Markets Act (DMA) Apple-কে অন্য browser engine অনুমতি দিতে বাধ্য করে। এর প্রস্তুতিতে iOS 17.4-এর beta সংস্করণে দেখা গেল, EU-তে হোম স্ক্রিনের ওয়েব অ্যাপগুলো আর নিজের জানালায় খুলছে না, সাধারণ Safari ট্যাব হয়ে যাচ্ছে, notification আর আলাদা storage হারিয়ে। Apple জানায়, অন্য engine-এর জন্য নিরাপদভাবে এই সুবিধা দেওয়া জটিল, তাই তারা তা সরিয়ে দিচ্ছে। ডেভেলপার আর ওয়েব-সংগঠনগুলোর প্রবল প্রতিবাদ আর নিয়ন্ত্রকদের প্রশ্নের পর, মার্চ ২০২৪-এর শুরুতে Apple সিদ্ধান্ত ফিরিয়ে নেয়: হোম স্ক্রিনের ওয়েব অ্যাপ EU-তেও থাকবে, তবে WebKit-এর ওপরই।
এই ঘটনা দেখায় একটি “খোলা” প্ল্যাটফর্মও কতটা নির্ভর করে কয়েকটি কোম্পানির সিদ্ধান্তের ওপর। ব্যবহারিক শিক্ষা: Background Sync না থাকলে online event, push না থাকলে ইমেইল বা SMS — মূল কাজ যেন কোনো একটি ব্রাউজারের সুবিধার ওপর পুরো নির্ভর না করে। বাংলাদেশে Android ফোন বেশি, তাই Chromium-এর সুবিধা অনেককে পাবে; তবু iPhone ব্যবহারকারীর জন্য অ্যাপটি ভাঙা চলবে না।
মূল উপলব্ধি: ওয়েবের ক্ষমতা শুধু মানদণ্ডে নয়, প্ল্যাটফর্ম মালিকদের ব্যবসায়িক সিদ্ধান্ত আর আইনেও নির্ধারিত হয়। Feature detection আর বিকল্প পথ রাখা তাই শুধু ভালো অভ্যাস নয়, টিকে থাকার কৌশল।
প্র ০৪দামি মোবাইল ডেটা আর অনিয়মিত 2G/3G-র বাজারে “offline-first” নকশা কেন শুধু একটি বাড়তি সুবিধা নয়, বরং মূল প্রয়োজন? বাংলাদেশের একটি অ্যাপের জন্য এর অর্থ কী?
সিলিকন ভ্যালির অফিসে বসে বানানো অনেক ওয়েবসাইট ধরে নেয়: নেটওয়ার্ক দ্রুত, স্থির আর বিনামূল্যে, ফোন শক্তিশালী। দক্ষিণ এশিয়া বা আফ্রিকার কোটি কোটি ব্যবহারকারীর বাস্তবতা ভিন্ন: ডেটা কিনতে হয় ছোট প্যাকেজে, মেয়াদ শেষ হয়ে যায়, শহরের বাইরে 4G থেকে নেমে 3G বা 2G, ভিড়ের সময় নেটওয়ার্ক জ্যাম, আর ফোনগুলো কম memory-র। এই কারণেই Google, Facebook আর Twitter-এর মতো কোম্পানি “Lite” সংস্করণ বানিয়েছে, আর Flipkart Lite আর Twitter Lite PWA হিসেবে তৈরি হয়েছে — উদ্দেশ্য ছিল ঠিক এই ব্যবহারকারীদের।
এমন বাজারে “অনলাইন-প্রথম” অ্যাপের প্রতিটি ছোট ব্যর্থতা ব্যবহারকারী হারানোর কারণ। একটি ফর্ম পূরণ করতে পাঁচ মিনিট লাগল, জমা দেওয়ার মুহূর্তে সংযোগ কাটল, আর সব লেখা হারিয়ে গেল — সেই ব্যবহারকারী আর ফেরেন না। প্রতিবার অ্যাপ খুলতে ২ MB JavaScript নতুন করে নামলে মাসের শেষে তা ডেটা প্যাকেজের একটি বড় অংশ। Offline-first এর উল্টোটা ধরে নেয়: নেটওয়ার্ক না থাকাই স্বাভাবিক, থাকলে বোনাস। অ্যাপ আগে স্থানীয়ভাবে লেখে-পড়ে, ব্যবহারকারীকে কখনো আটকায় না, আর পেছনে সুযোগমতো সার্ভারের সাথে মেলায়।
বাংলাদেশের প্রেক্ষাপটে কয়েকটি উদাহরণ ভাবুন। একজন মাঠপর্যায়ের স্বাস্থ্যকর্মী গ্রামে ঘুরে রোগীর তথ্য তোলেন — offline-first না হলে অ্যাপটিই অচল। একটি ছোট দোকানের হিসাবের অ্যাপ লোডশেডিংয়ে রাউটার বন্ধ থাকলেও বিক্রি লিখতে পারতে হবে। আবার কিছু কাজ অফলাইনে করা উচিতই নয় — মোবাইল ব্যাংকিংয়ের লেনদেন বা পেমেন্ট নিশ্চিতকরণ; সেখানে স্পষ্ট বার্তাই সঠিক নকশা: “সংযোগ নেই, লেনদেন হয়নি।”
মূল উপলব্ধি: আপনার ব্যবহারকারীর নেটওয়ার্কই আপনার অ্যাপের আসল পরিবেশ। যে বাজারে সংযোগ দামি আর অনিয়মিত, সেখানে service worker আর IndexedDB দিয়ে offline-first নকশা ব্যবসায়িক প্রয়োজন, প্রযুক্তিগত বিলাসিতা নয়।
প্র ০৫২০১৩ সালের “Extensible Web Manifesto” কী বলেছিল, আর service worker, Cache API ও fetch কীভাবে সেই দর্শনের সরাসরি ফল?
২০১৩ সালের জুনে ওয়েব মানদণ্ড নিয়ে কাজ করা একদল মানুষ — যাঁদের মধ্যে ছিলেন Alex Russell, Yehuda Katz-সহ ব্রাউজার আর framework জগতের পরিচিত নাম — একটি ছোট ঘোষণাপত্র প্রকাশ করেন: “The Extensible Web Manifesto”. এর মূল কথা চারটি। নতুন উচ্চ-স্তরের feature-এর আগে নিচু স্তরের, নিরাপদ, দক্ষ মৌলিক ক্ষমতা যোগ করা উচিত। বিদ্যমান উচ্চ-স্তরের feature-গুলোকে ব্যাখ্যা করা উচিত — অর্থাৎ দেখানো উচিত যে এগুলো ওই মৌলিক ক্ষমতা দিয়েই বানানো যায়। ডেভেলপাররা library-তে যে সমাধান বানান আর বহুল ব্যবহার করেন, সেগুলোই পরে মানদণ্ডে উঠে আসা উচিত। আর মানদণ্ড সংস্থাগুলো এই নীতি মেনে চলা কাজকেই অগ্রাধিকার দেবে।
এই দর্শনের পটভূমি ছিল AppCache-এর মতো অভিজ্ঞতা — কমিটিতে বসে ঠিক করা একটি উচ্চ-স্তরের সমাধান, যা বাস্তবে ব্যর্থ হলো আর বদলানোর উপায় রইল না। সমাধান হিসেবে প্রস্তাব এল: ব্রাউজারের ভেতরের “জাদু”-গুলো খুলে দাও। Service worker ঠিক তা-ই। এটি ব্রাউজারের network স্তরের একটি hook, যা আগে শুধু ব্রাউজারের C++ কোডের অধিকারে ছিল। Cache API সেই HTTP cache-এর একটি প্রোগ্রাম করা যায় এমন রূপ। আর fetch, Request, Response class-গুলো (পর্ব ৩৫) প্রথমবারের মতো ব্রাউজার যেভাবে ভেতরে অনুরোধ আর উত্তর দেখে, সেভাবেই JavaScript-কে দেখার সুযোগ দিল; Fetch মানদণ্ড গোছানোর পেছনে service worker-এর প্রয়োজন বড় ভূমিকা রেখেছিল।
দর্শনের তৃতীয় নীতির প্রমাণও দেখা যায়। ডেভেলপাররা হাতে stale-while-revalidate, network-first লিখতেন; Google-এর sw-toolbox আর পরে Workbox সেগুলো library-তে গোছাল; নামগুলো আজ সবার সাধারণ ভাষা। একই দর্শন থেকে এসেছে Web Components-এর Custom Elements, CSS Houdini, আর Streams API — সবগুলোই নিচু স্তরের যন্ত্র, যার ওপর সমাজ নিজের সমাধান বানায়।
মূল উপলব্ধি: প্ল্যাটফর্ম নকশায় “কী সমাধান দেব” প্রশ্নের চেয়ে “কোন মৌলিক ক্ষমতা খুলে দেব, যাতে মানুষ নিজের সমাধান বানাতে পারে” প্রশ্নটি বেশি টেকসই। আপনার নিজের API বা library নকশাতেও এই নীতি কাজে লাগে।
অনুশীলন
বেশিরভাগ কাজ ভাবনার আর DevTools-এ দেখার — ফল আগে অনুমান করুন, তারপর মিলিয়ে নিন।
-
Strategy বেছে নিন: একটি অনলাইন বাজারের PWA-তে প্রতিটি অনুরোধের জন্য কোন strategy? (ক)
/assets/app.8b2e7d.js(খ)/api/products?category=rice(গ)POST /api/orders(ঘ)/api/bkash/confirm(ঙ) হোম পাতা/(চ)/api/me/orders— লগইন-করা ব্যবহারকারীর অর্ডারের তালিকা।(ক) cache-first — নামে hash আছে, বিষয়বস্তু বদলালে নামই বদলাবে; precache-এও রাখা যায়। (খ) stale-while-revalidate — তালিকা সাথে সাথে দেখানো, দাম একটু পুরোনো হলেও অর্ডারের সময় সার্ভার যাচাই করবে। (গ) ধরবেন না — POST cache হয় না; অফলাইনে IndexedDB-র outbox আর Background Sync (বা
onlineevent). (ঘ) network-only — পেমেন্ট কখনো cache থেকে নয়, অফলাইনে স্পষ্ট বার্তা। (ঙ) network-first, timeout আর অফলাইনে cache বা/offline.html— নতুন deploy যাতে সবাই পান। (চ) ব্যক্তিগত ডেটা — সবচেয়ে নিরাপদ network-only; অফলাইনে দেখাতেই হলে IndexedDB-তে রাখুন আর লগআউটে মুছুন। -
বাগ খুঁজুন: AI-এর লেখা এই service worker-এ অন্তত চারটি সমস্যা আছে। কী কী?
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("fetch", (e) => e.respondWith(
caches.match(e.request).then((r) => r || fetch(e.request).then((res) => {
caches.open("cache").then((c) => c.put(e.request, res)); return res; }))));(১)
resএকই সাথে cache-এ আর পাতায় যাচ্ছে — body একবারই পড়া যায়, তাইres.clone()দিয়ে কপি cache-এ দিতে হবে; নইলে একটি পক্ষ “body already used” ধরনের TypeError পাবে। (২) সব অনুরোধ ধরা হচ্ছে — POST-এcache.putTypeError দেবে, আর লগইন বা ব্যক্তিগত উত্তরও জমবে;req.method !== "GET"আর ব্যক্তিগত পথ বাদ দিন। (৩)res.okদেখা নেই —404/500-ও চিরকালের জন্য জমবে। (৪) Cache-এর নামে সংস্করণ নেই আর activate-এ পুরোনো cache মোছা নেই; সব কিছু cache-first হওয়ায় HTML-ও কখনো হালনাগাদ হবে না। (৫) অন্ধskipWaiting— পুরোনো পাতা নতুন service worker-এর হাতে পড়বে। -
DevTools অভিযান: পরিচিত কোনো বড় সাইট খুলুন (অনেক খবরের সাইট বা X-এর মোবাইল সাইট service worker ব্যবহার করে)। F12 → Application → Service workers: কোনো service worker আছে কি? Cache storage-এ কী কী নামের cache? তারপর Network tab-এ “Offline” দিয়ে পাতা রিফ্রেশ করুন — কী দেখায়?
যে সাইটে service worker আছে, সেখানে Application প্যানেলে তার ঠিকানা (প্রায়ই
/sw.jsবা build টুলের দেওয়া নাম), অবস্থা (activated and is running) আর scope দেখবেন। Cache storage-এ প্রায়ইworkbox-precache-v2-…ধরনের নাম থাকে — বুঝবেন সাইটটি Workbox ব্যবহার করে — আর runtime cache-এর জন্য আলাদা নাম। Offline-এ রিফ্রেশ করলে ভালো PWA নিজের একটি অফলাইন পাতা বা আগে দেখা কনটেন্ট দেখায়; service worker না থাকলে ব্রাউজারের “No internet” পাতা। -
নিজের প্রথম PWA (কোড): একটি ছোট “অফলাইন খাতা” বানান —
index.html-এ একটি খরচ লেখার ফর্ম আর তালিকা (আপাতত localStorage-এ রাখুন), এই পর্বের manifest, অফলাইন পাতা আর install/activate/fetch-সহsw.js. localhost-এ চালিয়ে DevTools-এর Offline দিয়ে পরীক্ষা করুন। তারপর CSS-এ রং বদলে cache-এর সংস্করণ v1 থেকে v2 করুন — আপডেট কখন দেখা যায়?প্রথম লোডে Application → Service workers-এ v1 “activated” দেখাবে, Cache storage-এ
khata-shell-v1. Offline চালু করে রিফ্রেশ করলে পাতা cache থেকে খুলবে, আর আগে লেখা খরচও থাকবে (localStorage ফোনেই থাকে)। CSS বদলে শুধু CSS ফাইল সার্ভারে দিলে কিছুই বদলাবে না — cache-first সেটিকে আটকে রেখেছে, আরsw.jsএক byte-ও বদলায়নি বলে নতুন install-ও হয়নি। সংস্করণ v2 করলেsw.jsবদলায়: রিফ্রেশে v2 install হয়ে “waiting to activate” অবস্থায় বসে থাকবে, পাতায় পুরোনো রং। ট্যাব বন্ধ করে আবার খুললে (বা “skipWaiting” চাপলে) v2 active, নতুন রং, আরkhata-shell-v1মুছে গেছে। এটিই অংশ ৪-এর স্টেপার, নিজের চোখে। শেষে CSS ফাইলের নামে hash দিন, যাতে সংস্করণ ভুলে গেলেও পুরোনো ফাইল আটকে না থাকে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকজন ব্যবহারকারী প্রথমবার একটি PWA-তে এলেন, তারপর আবার এলেন। Service worker-এর ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- পাতা navigator.serviceWorker.register("/sw.js") ডাকে
- install event-এ app shell-এর ফাইলগুলো cache-এ জমা হয়
- activate event-এ পুরোনো সংস্করণের cache মোছা হয়
- পরের লোডে পাতাটি service worker-এর নিয়ন্ত্রণে আসে
- পাতার প্রতিটি অনুরোধ fetch event হয়ে service worker-এ পৌঁছায়
একটি সাইটে sw.js-এর নতুন সংস্করণ তোলা হলো। ব্যবহারকারীর তিনটি ট্যাব খোলা, তিনি একটি ট্যাব রিফ্রেশ করলেন। নতুন service worker-এর কী অবস্থা হবে (skipWaiting ছাড়া)?
- সাথে সাথে active হয়ে তিনটি ট্যাবই নিয়ন্ত্রণ করবে
- ব্রাউজার নতুন সংস্করণ উপেক্ষা করবে, ২৪ ঘণ্টা পর দেখবে
- install হয়ে waiting অবস্থায় থাকবে, সব ট্যাব বন্ধ হলে active হবে
- পুরোনো service worker unregister হয়ে সাইট SW ছাড়া চলবে
Service worker-এ network থেকে আসা একটি Response cache-এও রাখতে হবে আর পাতাকেও দিতে হবে। সঠিক উপায় কোনটি?
cache.put(req, res.clone())করে আসলresপাতায় ফেরত দেওয়া- একই
resদিয়েcache.putআরreturn res JSON.stringify(res)cache-এ রাখা- দুবার
fetchকরে একটি cache-এ, একটি পাতায়
একটি অনলাইন বাজারের পেমেন্ট নিশ্চিত করার API (/api/pay/confirm)-এর জন্য service worker-এ কোন strategy উপযুক্ত?
- Cache-first
- Stale-while-revalidate
- Cache-only
- Network-only — কখনো cache নয়
Stale-while-revalidate strategy-তে cache-এ একটি কপি থাকলে কী ঘটে?
- আগে network-এর জন্য অপেক্ষা, ব্যর্থ হলে cache
- cache-এর কপি সাথে সাথে দেওয়া হয়, আর পেছনে network থেকে এনে cache হালনাগাদ হয়
- cache-এর কপি দেওয়া হয়, network-এ কখনো যাওয়া হয় না
- cache আর network দুটোর উত্তর মিলিয়ে নতুন উত্তর বানানো হয়
একটি সাইট সাধারণ http:// ঠিকানায় (localhost নয়) চলছে। সেখানে service worker নিবন্ধনের চেষ্টা করলে কী হবে?
- স্বাভাবিকভাবে নিবন্ধিত হবে
- ব্রাউজার নিজে থেকে https-এ নিয়ে নিবন্ধন করবে
- নিবন্ধন হবে না — service worker শুধু HTTPS বা localhost-এ চলে
- নিবন্ধিত হবে, কিন্তু শুধু push পাবে
অফলাইনে দেওয়া একটি অর্ডার সংযোগ ফিরলে পাঠাতে Background Sync ব্যবহার করা হচ্ছে। কোন কথাটি সঠিক?
- এটি সব ব্রাউজারে নেই, তাই feature detection আর
onlineevent-এর বিকল্প পথ রাখতে হয় - সব আধুনিক ব্রাউজারে আছে, বিকল্পের দরকার নেই
- অর্ডারটি Cache API-তে POST হিসেবে জমিয়ে রাখা হয়
- পাতা খোলা না থাকলে sync event কখনো আসে না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- PWA = সাধারণ ওয়েবসাইট + HTTPS + manifest + service worker; ইনস্টল হয়, দ্রুত খোলে, অফলাইনে চলে — আর পুরোনো ব্রাউজারে সাধারণ সাইট।
- Service worker হলো পাতা আর network-এর মাঝের প্রোগ্রাম করা proxy; একটি origin-এ একটি, DOM নেই, পাতা বন্ধ থাকলেও event পায়।
- জীবনচক্র: register → install (precache) → waiting → activate (পুরোনো cache মোছা) → fetch. skipWaiting শুধু ব্যবহারকারীর সম্মতিতে।
- Static hashed ফাইলে cache-first, HTML/API-তে network-first, তালিকায় stale-while-revalidate, পেমেন্টে network-only; cache-এ রাখার আগে
clone(). - POST আর ব্যক্তিগত উত্তর cache নয়; ডেটা IndexedDB-তে, পাঠানো Background Sync বা
onlineevent-এ; cache-এর নামে সংস্করণ আর একটি kill switch পরিকল্পনা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৫ · module — import/export, ESM বনাম CommonJSপরবর্তী পর্বকোড ভাগ করা, আমদানি-রপ্তানি আর module লোডের নিয়ম।
- JavaScript প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Mobile App Development · Offline-first ডেটা কৌশলগভীরে যানমোবাইল অ্যাপে অফলাইন ডেটা আর সিঙ্ক করার নকশা।
- System Design · Caching strategy ও patternপাশের ট্র্যাকএকই cache-first, write-through ধারণা বড় সিস্টেমের স্তরে।
- Computer Networksপাশের ট্র্যাকHTTP, HTTPS, proxy আর CDN — service worker-এর নিচের জগৎ।