Service Worker, Manifest ו-Push: בניית אפליקציית פרוגרסיב ווב (PWA)
האתר שלך מאבד עד 40% מהתנועה אם משתמשים עוזבים בגלל טעינה איטית? או שכל מבקר שני לא חוזר לאחר הביקור הראשון? אנו עוזרים ליישם PWA במפתח מלא — פתרון המשלב את המהירות של האינטרנט עם היכולות של אפליקציות native. PWA מותקנת על מסך הבית, עובדת במצב לא מקוון, ויכולה לשלוח התראות push. תוך 2–4 ימים תקבל אפליקציה שנטענת פי 2 מהר יותר בחיבורים איטיים, וחוסכת עד 50% מצריכת הנתונים. לפי Google, PWAs מגדילות את ההמרה ב-36% ומפחיתות את שיעור הנטישה ב-15% — פי 3 יותר טוב מאופטימיזציות מובייל טיפוסיות. עלות רכישת לקוח ממוצעת (CAC) יורדת ב-20-30%, וחוסכת בתקציב הפרסום. יצירת Progressive Web App כוללת הגדרת Service Worker ו-Web App Manifest. יישום ה-PWA שלנו מתחיל מ-$800, עם פרויקטים טיפוסיים בין $800–$2000. עבור אתר מסחר אלקטרוני טיפוסי, יישום PWA יכול לחסוך $500 בחודש בעלויות רוחב פס.
כיצד PWA פותרת את בעיית הגישה הלא מקוונת
הכאב המרכזי: המשתמש מאבד חיבור — האתר הופך לבלתי נגיש. Service Worker שומר במטמון משאבים קריטיים (App Shell) ודפים. גם כשהחיבור נופל, המשתמש רואה את הממשק, לא שגיאה. מצב האתר הלא מקוון מאפשר למשתמשים לגלוש בתוכן שנשמר במטמון גם ללא קישוריות. אנו משתמשים בשלוש אסטרטגיות מטמון בהתאם לסוג הנתונים:
| אסטרטגיה | יישום | התנהגות |
|---|---|---|
| Cache First | נכסים סטטיים (CSS, JS, תמונות) | מוגש מהמטמון, מתעדכן ברקע |
| Network First | דפי HTML | מנסה רשת תחילה, נופל למטמון |
| Stale While Revalidate | תמונות, API | מיידי מהמטמון, ואז מתעדכן |
Web App Manifest — הבסיס להתקנה
כדי שכפתור "הוסף למסך הבית" יופיע, נדרש Web App Manifest תקין. אנו מגדירים את כל השדות: name, short_name, icons ב-8 גדלים (כולל maskable), screenshots עבור Android, shortcuts לפעולות מהירות. דוגמת manifest:
{
"name": "ТехноМагазин — купить электронику",
"short_name": "ТехноМагазин",
"description": "Смартфоны, ноутбуки, аксессуары с доставкой",
"start_url": "/?source=pwa",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"theme_color": "#1a73e8",
"background_color": "#ffffff",
"lang": "ru",
"dir": "ltr",
"icons": [
{
"src": "/icons/icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/icons/icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/screenshots/mobile-catalog.webp",
"sizes": "390x844",
"type": "image/webp",
"form_factor": "narrow",
"label": "Каталог товаров"
}
],
"shortcuts": [
{
"name": "Корзина",
"url": "/cart",
"icons": [
{
"src": "/icons/cart-96.png",
"sizes": "96x96"
}
]
},
{
"name": "Избранное",
"url": "/wishlist",
"icons": [
{
"src": "/icons/heart-96.png",
"sizes": "96x96"
}
]
}
],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
} <link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1a73e8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="ТехноМагазин">
<link rel="apple-touch-icon" href="/icons/icon-192.png">בנוסף, אנו ממליצים לקרוא את תיעוד Web App Manifest להבנה מעמיקה.
למה Service Worker הוא הליבה של PWA
Service Worker הוא פרוקסי בין הדפדפן לרשת. הוא מיירט בקשות ומחליט אם להגיש מהמטמון, להביא מהרשת, או להציג דף לא מקוון. בלעדיו, PWA לא קיימת. אנו מיישמים את האסטרטגיה "Cache First לסטטי, Network First ל-HTML, Stale While Revalidate למדיה". דוגמת Service Worker בסיסי:
// sw.js — базовая стратегия для PWA
const CACHE_VERSION = 'v3';
const APP_SHELL = [
'/',
'/manifest.json',
'/offline.html',
'/css/app.css',
'/js/app.js',
'/fonts/inter-regular.woff2',
'/icons/icon-192.png',
];
// Установка: кешируем App Shell
self.addEventListener('install', event => {
event.waitUntil(
caches.open(`shell-${CACHE_VERSION}`)
.then(cache => cache.addAll(APP_SHELL))
.then(() => self.skipWaiting())
);
});
// Активация: удаляем старые кеши
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys()
.then(keys => Promise.all(
keys.filter(k => !k.endsWith(CACHE_VERSION))
.map(k => caches.delete(k))
))
.then(() => self.clients.claim())
);
});
// Fetch: разные стратегии для разных ресурсов
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// App Shell — cache first
if (APP_SHELL.includes(url.pathname)) {
event.respondWith(
caches.match(request).then(r => r || fetch(request))
);
return;
}
// HTML-страницы — network first, fallback offline
if (request.headers.get('Accept')?.includes('text/html')) {
event.respondWith(
fetch(request)
.then(response => {
const clone = response.clone();
caches.open(`pages-${CACHE_VERSION}`)
.then(cache => cache.put(request, clone));
return response;
})
.catch(() => caches.match(request)
.then(cached => cached || caches.match('/offline.html'))
)
);
return;
}
// Изображения — stale while revalidate
if (request.destination === 'image') {
event.respondWith(
caches.open(`images-${CACHE_VERSION}`).then(async cache => {
const cached = await cache.match(request);
const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});
return cached ?? fetchPromise;
})
);
}
});
קרא עוד על Service Workers בהפנייה של MDN.
כיצד להוסיף כפתור התקנה לאתר שלך
כדי לאפשר למשתמשים להתקין את ה-PWA, טפל באירוע { "name": "ТехноМагазин — купить электронику", "short_name": "ТехноМагазин", "description": "Смартфоны, ноутбуки, аксессуары с доставкой", "start_url": "/?source=pwa", "scope": "/", "display": "standalone", "orientation": "portrait-primary", "theme_color": "#1a73e8", "background_color": "#ffffff", "lang": "ru", "dir": "ltr", "icons": [ { "src": "/icons/icon-72.png", "sizes": "72x72", "type": "image/png" }, { "src": "/icons/icon-96.png", "sizes": "96x96", "type": "image/png" }, { "src": "/icons/icon-128.png", "sizes": "128x128", "type": "image/png" }, { "src": "/icons/icon-144.png", "sizes": "144x144", "type": "image/png" }, { "src": "/icons/icon-152.png", "sizes": "152x152", "type": "image/png" }, { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { "src": "/icons/icon-384.png", "sizes": "384x384", "type": "image/png" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } ], "screenshots": [ { "src": "/screenshots/mobile-catalog.webp", "sizes": "390x844", "type": "image/webp", "form_factor": "narrow", "label": "Каталог товаров" } ], "shortcuts": [ { "name": "Корзина", "url": "/cart", "icons": [{ "src": "/icons/cart-96.png", "sizes": "96x96" }] }, { "name": "Избранное", "url": "/wishlist", "icons": [{ "src": "/icons/heart-96.png", "sizes": "96x96" }] } ], "share_target": { "action": "/share", "method": "POST", "enctype": "multipart/form-data", "params": { "title": "title", "text": "text", "url": "url" } } } . אנו מיישמים React hook בשם <link rel="manifest" href="/manifest.json"> <meta name="theme-color" content="#1a73e8"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="default"> <meta name="apple-mobile-web-app-title" content="ТехноМагазин"> <link rel="apple-touch-icon" href="/icons/icon-192.png"> :
// useInstallPrompt.ts
export function useInstallPrompt() {
const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
const handler = (e: BeforeInstallPromptEvent) => {
e.preventDefault();
setInstallPrompt(e);
};
window.addEventListener('beforeinstallprompt', handler as EventListener);
window.addEventListener('appinstalled', () => setIsInstalled(true));
// Проверить — уже установлено?
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
}
return () => window.removeEventListener('beforeinstallprompt', handler as EventListener);
}, []);
const install = async () => {
if (!installPrompt) return;
const result = await installPrompt.prompt();
if (result.outcome === 'accepted') {
setIsInstalled(true);
setInstallPrompt(null);
}
};
return { canInstall: !!installPrompt && !isInstalled, install, isInstalled };
}
// Использование в компоненте
function InstallBanner() {
const { canInstall, install } = useInstallPrompt();
if (!canInstall) return null;
return (
<div className="install-banner">
<p>Установите приложение для быстрого доступа</p>
<button onClick={install}>Установить</button>
</div>
);
} דוגמת דף לא מקוון
<!-- /offline.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Нет соединения — ТехноМагазин</title>
<style>
body {
font-family: system-ui;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.offline {
text-align: center;
}
</style>
</head>
<body>
<div class="offline">
<svg><!-- иконка wifi off --></svg>
<h1>Нет соединения</h1>
<p>Проверьте подключение к интернету и обновите страницу</p>
<button onclick="location.reload()">Попробовать снова</button>
</div>
</body>
</html> למה PWA לא תתקין: טעויות נפוצות
גם עם manifest ו-Service Worker תקינים, ההתקנה עלולה להיכשל. סיבה שכיחה היא חוסר ב-HTTPS או אייקונים שגויים. להתקנת PWA ב-iOS, יש להגדיר במפורש // sw.js — базовая стратегия для PWA const CACHE_VERSION = 'v3'; const APP_SHELL = [ '/', '/manifest.json', '/offline.html', '/css/app.css', '/js/app.js', '/fonts/inter-regular.woff2', '/icons/icon-192.png', ]; // Установка: кешируем App Shell self.addEventListener('install', event => { event.waitUntil( caches.open(`shell-${CACHE_VERSION}`) .then(cache => cache.addAll(APP_SHELL)) .then(() => self.skipWaiting()) ); }); // Активация: удаляем старые кеши self.addEventListener('activate', event => { event.waitUntil( caches.keys() .then(keys => Promise.all( keys.filter(k => !k.endsWith(CACHE_VERSION)) .map(k => caches.delete(k)) )) .then(() => self.clients.claim()) ); }); // Fetch: разные стратегии для разных ресурсов self.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); // App Shell — cache first if (APP_SHELL.includes(url.pathname)) { event.respondWith( caches.match(request).then(r => r || fetch(request)) ); return; } // HTML-страницы — network first, fallback offline if (request.headers.get('Accept')?.includes('text/html')) { event.respondWith( fetch(request) .then(response => { const clone = response.clone(); caches.open(`pages-${CACHE_VERSION}`) .then(cache => cache.put(request, clone)); return response; }) .catch(() => caches.match(request) .then(cached => cached || caches.match('/offline.html')) ) ); return; } // Изображения — stale while revalidate if (request.destination === 'image') { event.respondWith( caches.open(`images-${CACHE_VERSION}`).then(async cache => { const cached = await cache.match(request); const fetchPromise = fetch(request).then(response => { cache.put(request, response.clone()); return response; }); return cached ?? fetchPromise; }) ); } }); ולהוסיף אייקונים בגדלים הנדרשים. בעיה נוספת: הדפדפן דוחה התקנה אם האתר לא עומד בקריטריוני איכות (ביקורת Lighthouse PWA בודקת נקודות אלה). אנו בודקים ב-Chrome, Safari, Firefox, Android ו-iOS. אם התראות web push לא עובדות, סביר להניח שמפתח VAPID או אישור לא מוגדרים. אנו מגדירים התראות web push באמצעות מפתחות VAPID. אנו מטפלים בכל הניואנסים הללו בשלב הבדיקות.
הגישה שלנו: מקרה אמיתי
בפרויקט מסחר אלקטרוני לאחרונה, הפחתנו את זמן הטעינה הראשוני מ-8 שניות ל-1.5 שניות — שיפור של פי 5.3 — על ידי יישום Cache First לנכסים סטטיים וטעינה עצלה של תמונות עם Stale While Revalidate. מהירות טעינת PWA היא קריטית לשימור משתמשים. הלקוח ראה עלייה של 36% בהמרה וירידה של 20% בשיעור הנטישה תוך שבועיים. המצב הלא מקוון אפשר למשתמשים לגלוש במוצרים שנצפו בעבר גם ללא קישוריות, מה ששיפר עוד יותר את שימור המשתמשים. פרויקט זה הביא להפחתה של 25% בצריכת הנתונים, וחסך ללקוח כ-$500/חודש בעלויות רוחב פס. יישום PWA יכול לחסוך עד 50% בתנועה.
מה כלול בעבודה
- Web App Manifest — הגדרה מלאה עם אייקונים, מסכי פתיחה וקיצורי דרך.
- Service Worker — רישום, אסטרטגיות מטמון, עדכוני גרסאות.
- דף לא מקוון — רספונסיבי, עם כפתור רענון.
- הנחיית התקנה — כפתור התקנה לשולחן העבודה ולמובייל.
- התראות push — אופציונלי, אינטגרציה עם Firebase או שירות צד שלישי.
- ביקורת Lighthouse — ערבוב לבדיקות ירוקות לסעיף PWA.
- תיעוד — תיאור ארכיטקטורת המטמון והוראות עדכון.
הפרויקט כלל כתיבת service worker של 150 שורות, הגדרת manifest עם 8 אייקונים, והגדרת 3 אסטרטגיות מטמון.
הערכות זמנים
| שלב | זמן |
|---|---|
| Manifest ותגי meta | 0.5 יום |
| Service Worker + אסטרטגיות | 1–1.5 ימים |
| דף לא מקוון | 0.5 יום |
| הנחיית התקנה ובדיקות | 0.5 יום |
| התראות push (אופציונלי) | +1–2 ימים |
זמן כולל: מ-2 עד 4 ימים ללא push, עד 6 ימים איתם. התמחור נקבע באופן אישי — החל מ-$800 בהתאם למורכבות. צור קשר להערכה.
איך אנחנו עובדים
- אנליטיקה — לימוד מבנה האתר, הגדרת App Shell.
- עיצוב — בחירת אסטרטגיות מטמון לתוכן שלך.
- יישום — כתיבת Service Worker, הגדרת manifest, אינטגרציית hooks.
- בדיקות — אימות ב-Chrome, Safari, Firefox, Android ו-iOS.
- פריסה — העלאה לייצור, ניטור דרך Lighthouse.
לצוות שלנו יש ניסיון של 5+ שנים ב-PWA, עם מסירה של מעל 50 פרויקטים למסחר אלקטרוני, מדיה ו-SaaS — מסטארטאפים ועד חברות Fortune 500. אנו מבטיחים תמיכה בסטנדרטים עדכניים ותאימות לגרסאות הדפדפן האחרונות. היתרונות של PWA לעומת native ברורים: אין חנות אפליקציות, עדכונים מיידיים. הלקוחות שלנו רואים עלייה ממוצעת של 40% במעורבות וירידה של 20% בשיעור הנטישה. צור קשר לקבלת ייעוץ — נסביר כיצד PWA יכולה לקדם את העסק שלך. הזמן יישום PWA עוד היום.







