Розроблення фронтенду сайту на Vanilla JavaScript
Ванільний JavaScript — це нативний браузерний API без абстракцій поверху нього. Без фреймворка, без virtual DOM, без магії. Коли розробник пише Vanilla JS, він працює безпосередньо з тим, що браузер надає: document.querySelector, fetch, IntersectionObserver, CustomEvent, Web Components.
Це не ностальгія по доReact-епосі. Це свідомий вибір для проектів з конкретними вимогами: мінімальний бандл, максимальний контроль, нулеві залежності.
Коли Vanilla JS виправданий
- Віджети та embed-скрипти — код вбудовується на чужі сайти, не можна конфліктувати з їхніми фреймворками
- Бібліотеки для публікації в npm — залежність від React утяжелить бібліотеку для користувачів з Vue або Svelte
- Високонаванажені анімації — прямий доступ до Canvas API, WebGL, Web Animations API
- Розширення браузера — Content Scripts працюють в ізольованому середовищі, фреймворки додають ризики
- Статичні сайти з мінімальною інтерактивністю — незачем тащити 50 KB для одного дропдауну
Сучасний Vanilla JS — це не 2010 рік
Браузерний API за останні 10 років кардинально виріс:
// Ленива завантаження з IntersectionObserver
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
// Fetch з AbortController та таймаутом
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} finally {
clearTimeout(timeout);
}
}
Web Components — компонентна модель без фреймворка
class ProductCard extends HTMLElement {
static get observedAttributes() { return ['product-id']; }
connectedCallback() {
this.#render();
this.#attachEvents();
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'product-id' && oldVal !== newVal) {
this.#fetchProduct(newVal);
}
}
async #fetchProduct(id) {
const data = await fetchWithTimeout(`/api/products/${id}`);
this.#update(data);
}
#render() {
this.innerHTML = `
<article class="product-card">
<img class="product-card__img" alt="">
<h3 class="product-card__name"></h3>
<span class="product-card__price"></span>
<button class="product-card__btn">До корзини</button>
</article>
`;
}
#update({ name, price, image }) {
this.querySelector('.product-card__img').src = image;
this.querySelector('.product-card__name').textContent = name;
this.querySelector('.product-card__price').textContent = `${price} ₽`;
}
#attachEvents() {
this.querySelector('.product-card__btn').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('add-to-cart', {
bubbles: true,
detail: { productId: this.getAttribute('product-id') }
}));
});
}
}
customElements.define('product-card', ProductCard);
Використання в HTML: <product-card product-id="42"></product-card>. Працює в будь-якому фреймворку або без нього.
Управління станом без Redux
// Простий реактивний store через Proxy
function createStore(initialState) {
const listeners = new Set();
const state = new Proxy(structuredClone(initialState), {
set(target, key, value) {
target[key] = value;
listeners.forEach(fn => fn(structuredClone(target)));
return true;
}
});
return {
state,
subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); },
getSnapshot: () => structuredClone(state),
};
}
const cartStore = createStore({ items: [], total: 0 });
cartStore.subscribe(state => {
document.getElementById('cart-count').textContent = state.items.length;
});
Структура проекту з ESModules
src/
components/
product-card.js // Web Component
modal.js
lib/
store.js
api.js
utils.js
pages/
catalog.js
product.js
app.js
// package.json — мінімальний
{
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"vite": "^5.0.0"
}
}
Нульових runtime-залежностей. Vite тільки для розроблення та збирання.
Терміни реалізації
- Тиждень 1: архітектура модулів, базові Web Components, API-клієнт
- Тижні 2–3: бізнес-логіка, інтерактивні компоненти, анімації
- Тиждень 4: оптимізація (bundle splitting, prefetch), тестування (Vitest + jsdom)
Результуючий бандл для середнього проекту — 20–40 KB без зовнішніх залежностей. LCP < 1s навіть на повільному з'єднанні.







