Разработка фронтенда сайта на 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 даже на медленном соединении.







