Реалізація анімації завантаження (Preloader) сайту

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація анімації завантаження (Preloader) сайту
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація анімації завантаження сайту (Preloader)

Preloader — екран, що показується поки сторінка завантажується. Його завдання: приховати частково завантажений вміст, дати змогу програти вводну анімацію, створити відчуття намиреного початку. Поганий preloader дратує. Хороший — органічно переходить на перший екран.

Коли preloader виправданий

Preloader потрібен якщо: сайт завантажує важкі WebGL-сцени або відео перед показом, дизайн вимагає синхронної анімації тексту/логотипу при старті, або перший екран без контенту буде виглядати некоректно. Для звичайних інформаційних сайтів preloader — лишня затримка.

Базова структура

Preloader вставляється першим у <body>, працює на чистому CSS без JS-залежностей для начального рендеру:

<body>
  <div id="preloader" class="preloader">
    <div class="preloader__content">
      <div class="preloader__logo">
        <svg>...</svg>
      </div>
      <div class="preloader__counter">
        <span id="preloader-count">0</span>
        <span>%</span>
      </div>
    </div>
    <div class="preloader__overlay"></div>
  </div>

  <main id="app">
    <!-- Вміст прихований до закінчення preloader -->
  </main>
</body>
.preloader {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #0a0a0a;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* Вміст прихований під час завантаження */
body.is-loading #app {
  visibility: hidden;
}

Відслідковування прогресу завантаження

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()
})

Анімація логотипу

Типовий варіант: SVG логотип з path анімацією через stroke-dashoffset.

.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 з progress-баром та fade-out — 4–6 годин. Анімація логотипу, відслідковування реальних ресурсів, плавний перехід до першого екрану — 1–2 дні.