Vanilla JavaScript Frontend Website Development
Vanilla JavaScript is native browser API without abstractions on top. No framework, no virtual DOM, no magic. When developer writes Vanilla JS, they work directly with what browser provides: document.querySelector, fetch, IntersectionObserver, CustomEvent, Web Components.
This is not nostalgia for pre-React era. This is conscious choice for projects with specific requirements: minimal bundle, maximum control, zero dependencies.
When Vanilla JS is justified
- Widgets and embed scripts — code embedded on third-party sites, can't conflict with their frameworks
- Libraries for npm publication — React dependency will bloat library for Vue or Svelte users
- High-load animations — direct access to Canvas API, WebGL, Web Animations API
- Browser extensions — Content Scripts run in isolated environment, frameworks add risks
- Static sites with minimal interactivity — no need to drag 50 KB for one dropdown
Modern Vanilla JS is not 2010
Browser API in last 10 years grew dramatically:
// Lazy loading with 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 with AbortController and timeout
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 — component model without framework
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">Add to cart</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);
Usage in HTML: <product-card product-id="42"></product-card>. Works in any framework or without one.
State management without Redux
// Simple reactive store via 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;
});
Project structure with 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 — minimal
{
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"vite": "^5.0.0"
}
}
Zero runtime dependencies. Vite only for development and build.
Implementation timeline
- Week 1: module architecture, basic Web Components, API client
- Weeks 2–3: business logic, interactive components, animations
- Week 4: optimization (bundle splitting, prefetch), testing (Vitest + jsdom)
Final bundle for average project — 20–40 KB without external dependencies. LCP < 1s even on slow connection.







