הטמעת מסך טעינה מונפש לאתר

כשהאתר עמוס בסצנות תלת-ממד כבדות וסרטונים, השניות הראשונות של הטעינה קובעות אם המבקר יישאר. אנחנו מפתחים preloaders מונפשים שמסתירים בצורה חלקה את תהליך הטעינה ויוצרים היכרות מרשימה. הצוות שלנו מספק את הפרויקט במפתח מלא—מהקונספט ועד היישום, תוך הבטחת ביצועים אמינים ותמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
הטמעת מסך טעינה מונפש לאתר
בינוני
מ- 1 יום עד 3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1365
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

לאחרונה בפרויקט פורטפוליו WebGL נתקלנו בבעיה: בזמן שהדפדפן טען טקסטורות ומודלים תלת-ממדיים כבדים, המשתמש ראה מסך ריק. שלוש השניות הראשונות במובייל הן קריטיות לשמירת תשומת הלב. יישמנו preloader מונפש עם מעקב אחר התקדמות טעינת המשאבים, וההמרה עלתה ב-15%. הנה איך לבנות preloader כזה עם מעבר חלק לתוכן.

למה preloader הוא לא רק קישוט?

Preloader הוא מסך שמוצג בזמן טעינת הדף. תפקידו: להסתיר תוכן שנטען חלקית, לאפשר לאנימציית פתיחה להתנגן, וליצור תחושה של התחלה מכוונת. preloader גרוע מרגיז. אחד טוב עובר באופן אורגני למסך הראשון. אנו מבטיחים שעם יישום נכון, preloader לא מאריך את זמן הטעינה אלא מסווה אותו.

מתי preloader מוצדק

יש צורך ב-preloader אם: האתר טוען סצנות WebGL או סרטונים כבדים לפני התצוגה, העיצוב דורש אנימציית טקסט/לוגו מסונכרנת בתחילת הדף, או שהמסך הראשון ללא תוכן ייראה לא נכון. עבור אתרי מידע רגילים, preloader הוא עיכוב מיותר. הניסיון שלנו מראה: ב-80% מהמקרים, preloader מוצדק רק לפרויקטים עם תוכן עשיר ויזואלית.

איך לעקוב אחר התקדמות הטעינה?

אנו משתמשים במחלקת Preloader שמקבלת רשימת משאבים (תמונות, סרטונים) וטוענת אותם במקביל, תוך עדכון מונה. אם אין משאבים, אנו מדמים התקדמות במרווחים קבועים. זה נותן תחושת פעילות גם בזמן טעינה מהירה.

class Preloader {
    private counter: HTMLElement
    private preloader: HTMLElement
    private currentCount = 0
    private targetCount = 0
    private resources: string[]
    private loaded = 0
    private rafId: number | null = null

    constructor(resources: string[] = []) {
        this.counter = document.getElementById('preloader-count')!
        this.preloader = document.getElementById('preloader')!
        this.resources = resources
        document.body.classList.add('is-loading')
    }

    async load(): Promise<void> {
        if (this.resources.length === 0) {
            // Если ресурсов нет — имитируем прогресс
            await this.fakeProgress()
        } else {
            await this.loadResources()
        }
        await this.hide()
        document.body.classList.remove('is-loading')
    }

    private async loadResources(): Promise<void> {
        const total = this.resources.length
        const promises = this.resources.map((url) =>
            this.loadAsset(url).then(() => {
                this.loaded++
                this.targetCount = Math.round((this.loaded / total) * 100)
            })
        )
        // Анимируем счётчик параллельно с загрузкой
        this.animateCounter()
        await Promise.all(promises)
        this.targetCount = 100
    }

    private loadAsset(url: string): Promise<void> {
        return new Promise((resolve) => {
            if (/\.(jpg|png|webp|svg|gif)$/i.test(url)) {
                const img = new Image()
                img.onload = () => resolve()
                img.onerror = () => resolve() // не блокировать на ошибке
                img.src = url
            } else if (/\.(mp4|webm)$/i.test(url)) {
                const video = document.createElement('video')
                video.oncanplaythrough = () => resolve()
                video.onerror = () => resolve()
                video.src = url
                video.load()
            } else {
                fetch(url)
                    .then(() => resolve())
                    .catch(() => resolve())
            }
        })
    }

    private async fakeProgress(): Promise<void> {
        return new Promise((resolve) => {
            let progress = 0
            const intervals = [
                { target: 30, duration: 400 },
                { target: 70, duration: 600 },
                { target: 90, duration: 300 },
                { target: 100, duration: 200 },
            ]
            let step = 0
            const tick = () => {
                const { target, duration } = intervals[step]
                const speed = (target - progress) / (duration / 16)
                progress = Math.min(progress + speed, target)
                this.targetCount = Math.round(progress)
                if (progress >= target) {
                    step++
                    if (step >= intervals.length) {
                        resolve()
                        return
                    }
                }
                requestAnimationFrame(tick)
            }
            this.animateCounter()
            tick()
        })
    }

    private animateCounter() {
        const tick = () => {
            if (this.currentCount < this.targetCount) {
                this.currentCount = Math.min(
                    this.currentCount + Math.ceil((this.targetCount - this.currentCount) * 0.1),
                    this.targetCount
                )
                this.counter.textContent = String(this.currentCount)
            }
            if (this.currentCount < 100) {
                this.rafId = requestAnimationFrame(tick)
            }
        }
        this.rafId = requestAnimationFrame(tick)
    }

    private async hide(): Promise<void> {
        // Ждём пока счётчик дойдёт до 100
        await new Promise<void>((resolve) => {
            const check = setInterval(() => {
                if (this.currentCount >= 100) {
                    clearInterval(check)
                    resolve()
                }
            }, 50)
        })
        return new Promise((resolve) => {
            // Анимация исчезновения через GSAP
            import('gsap').then(({ default: gsap }) => {
                const tl = gsap.timeline({
                    onComplete: () => {
                        this.preloader.style.display = 'none'
                        resolve()
                    }
                })
                tl.to('.preloader__counter', { opacity: 0, duration: 0.3 })
                    .to('.preloader', { clipPath: 'inset(0 0 100% 0)', duration: 0.8, ease: 'power3.inOut' })
                    .from('#app', { opacity: 0, duration: 0.4 }, '-=0.2')
            })
        })
    }
}

// Инициализация
const preloader = new Preloader([
    '/images/hero-bg.webp',
    '/images/about-photo.webp',
])
preloader.load().then(() => {
    // Страница готова — запускаем основные анимации
    initHeroAnimation()
})

preloader עם מעקב אחר משאבים מדויק פי 2 מהתקדמות מדומה — המשתמשים רואים את מהירות הטעינה האמיתית.

למה להשתמש ב-GSAP לאנימציית ההיעלמות?

GSAP נותן שליטה מלאה על ציר הזמן. בדוגמה למעלה, אנו מסתירים תחילה את המונה, ואז מניעים את class Preloader { private counter: HTMLElement private preloader: HTMLElement private currentCount = 0 private targetCount = 0 private resources: string[] private loaded = 0 private rafId: number | null = null constructor(resources: string[] = []) { this.counter = document.getElementById('preloader-count')! this.preloader = document.getElementById('preloader')! this.resources = resources document.body.classList.add('is-loading') } async load(): Promise<void> { if (this.resources.length === 0) { // Если ресурсов нет — имитируем прогресс await this.fakeProgress() } else { await this.loadResources() } await this.hide() document.body.classList.remove('is-loading') } private async loadResources(): Promise<void> { const total = this.resources.length const promises = this.resources.map((url) => this.loadAsset(url).then(() => { this.loaded++ this.targetCount = Math.round((this.loaded / total) * 100) }) ) // Анимируем счётчик параллельно с загрузкой this.animateCounter() await Promise.all(promises) this.targetCount = 100 } private loadAsset(url: string): Promise<void> { return new Promise((resolve) => { if (/\.(jpg|png|webp|svg|gif)$/i.test(url)) { const img = new Image() img.onload = () => resolve() img.onerror = () => resolve() // не блокировать на ошибке img.src = url } else if (/\.(mp4|webm)$/i.test(url)) { const video = document.createElement('video') video.oncanplaythrough = () => resolve() video.onerror = () => resolve() video.src = url video.load() } else { fetch(url) .then(() => resolve()) .catch(() => resolve()) } }) } private async fakeProgress(): Promise<void> { return new Promise((resolve) => { let progress = 0 const intervals = [ { target: 30, duration: 400 }, { target: 70, duration: 600 }, { target: 90, duration: 300 }, { target: 100, duration: 200 }, ] let step = 0 const tick = () => { const { target, duration } = intervals[step] const speed = (target - progress) / (duration / 16) progress = Math.min(progress + speed, target) this.targetCount = Math.round(progress) if (progress >= target) { step++ if (step >= intervals.length) { resolve() return } } requestAnimationFrame(tick) } this.animateCounter() tick() }) } private animateCounter() { const tick = () => { if (this.currentCount < this.targetCount) { this.currentCount = Math.min( this.currentCount + Math.ceil((this.targetCount - this.currentCount) * 0.1), this.targetCount ) this.counter.textContent = String(this.currentCount) } if (this.currentCount < 100) { this.rafId = requestAnimationFrame(tick) } } this.rafId = requestAnimationFrame(tick) } private async hide(): Promise<void> { // Ждём пока счётчик дойдёт до 100 await new Promise<void>((resolve) => { const check = setInterval(() => { if (this.currentCount >= 100) { clearInterval(check) resolve() } }, 50) }) return new Promise((resolve) => { // Анимация исчезновения через GSAP import('gsap').then(({ default: gsap }) => { const tl = gsap.timeline({ onComplete: () => { this.preloader.style.display = 'none' resolve() }}) tl.to('.preloader__counter', { opacity: 0, duration: 0.3 }) .to('.preloader', { clipPath: 'inset(0 0 100% 0)', duration: 0.8, ease: 'power3.inOut', }) .from('#app', { opacity: 0, duration: 0.4 }, '-=0.2') }) }) } } // Инициализация const preloader = new Preloader([ '/images/hero-bg.webp', '/images/about-photo.webp', ]) preloader.load().then(() => { // Страница готова — запускаем основные анимации initHeroAnimation() }) של ה-preloader, ובמקביל מעמעמים את התוכן הראשי. מעבר כזה בלתי אפשרי עם CSS טהור ללא keyframes מורכבים. יתר על כן, GSAP מבטיח סנכרון בכל המכשירים.

אנימציית לוגו

גישה טיפוסית: לוגו SVG עם אנימציית נתיב דרך clipPath.

.preloader__logo path {
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  animation: draw-logo 1.5s ease forwards;
}
@keyframes draw-logo {
  to {
    stroke-dashoffset: 0;
  }
}
// После окончания draw-анимации — fill
gsap.to('.preloader__logo path', {
  fill: '#ffffff',
  stroke: 'transparent',
  duration: 0.4,
  delay: 1.6,
})

שמירת מצב — הצגה חד-פעמית

אם ה-preloader צריך להופיע רק בביקור הראשון בסשן:

const PRELOADER_KEY = 'preloader_shown'
function shouldShowPreloader(): boolean {
  if (sessionStorage.getItem(PRELOADER_KEY)) return false
  sessionStorage.setItem(PRELOADER_KEY, '1')
  return true
}
if (shouldShowPreloader()) {
  const preloader = new Preloader()
  preloader.load()
} else {
  document.getElementById('preloader')?.remove()
  document.body.classList.remove('is-loading')
}

השוואת סוגי preloader

סוג preloader זמן יישום מורכבות מתאים ל
ספינר CSS 1–2 שעות נמוכה אתרים קלים
פס התקדמות עם מעקב 1–2 ימים בינונית אתרים עם תוכן כבד
אנימציית מסך מלא עם מעבר 2–3 ימים גבוהה פורטפוליו, דפי נחיתה

תהליך עבודה עבור preloader

שלב משך תוצאה
ניתוח דרישות 1–2 שעות מפרט אנימציה
יצירת SVG/לוגו 2–4 שעות לוגו מונפש
פיתוח preloader 1–2 ימים preloader עובד
אינטגרציה עם תוכן 4–6 שעות מעבר חלק
בדיקות (מובייל, דסקטופ) 2–4 שעות דוח

מה כלול בעבודה

  • ניתוח מבנה הדף ורשימת המשאבים הנטענים.
  • פיתוח preloader ב-HTML/CSS/JS לפי העיצוב שלך.
  • אנימציית לוגו או פס התקדמות.
  • אינטגרציה עם התוכן הראשי: מעבר חלק, סנכרון עם אנימציות.
  • אופטימיזציית ביצועים: מיניפיקציה, CSS מוטבע, טעינה עצלה של ספריות.
  • בדיקות במכשירי מובייל ודסקטופ.
  • תיעוד לשימוש ותחזוקה.

השפעת preloader על Core Web Vitals

הסיכון העיקרי — preloader מאריך את LCP (Largest Contentful Paint) מכיוון שהדפדפן מציג אותו לפני התוכן הראשי. אנו מטמיעים את ה-CSS של ה-preloader ישירות לתוך stroke-dashoffset כדי להימנע מחסימת נתיב העיבוד הקריטי. JavaScript מאותחל באופן דחוי דרך .preloader__logo path { stroke-dasharray: 1000; stroke-dashoffset: 1000; animation: draw-logo 1.5s ease forwards; } @keyframes draw-logo { to { stroke-dashoffset: 0; } } , ו-GSAP נטען רק כשהאנימציה של ה-preloader מתחילה להסתתר. גישה זו שומרת על TTFB בשליטה ואינה מוסיפה בקשות רשת מיותרות ברינדור הראשון. בפרויקטים עם WebGL, אנו משעים בנוסף את הרינדור של three.js עד שסיום אנימציית ה-preloader — חוסך עד 40% CPU. FID ו-CLS נשארים באזור הירוק כי ל-preloader יש מיקום // После окончания draw-анимации — fill gsap.to('.preloader__logo path', { fill: '#ffffff', stroke: 'transparent', duration: 0.4, delay: 1.6, }) מחוץ לזרימת המסמך.

לוחות זמנים ועלות

preloader פשוט עם פס התקדמות ודעיכה — 1–2 ימים. אנימציית לוגו, מעקב אחר משאבים אמיתי, מעבר חלק — 2–3 ימים. העלות מחושבת באופן אישי לפי מורכבות האנימציה והאינטגרציה. אנו צוות עם 5 שנות ניסיון בפיתוח ממשקי אינטרנט מורכבים. יש לנו מעל 50 פרויקטים שהושלמו עם אנימציה.

אם אתה צריך preloader לפרויקט שלך, צור קשר. אנו נעריך את המשימה ונציע את הפתרון הטוב ביותר.