Vanilla JavaScript Frontend: Fast Widgets and Animations
When your application bundle exceeds 2 MB and LCP goes beyond 3 seconds, the cause is often framework bloat. For embed widgets, animations, or browser extensions, Vanilla JavaScript becomes the only adequate choice. We have delivered over 30 projects without a single framework, ensuring LCP under 1 second and bundle sizes of 20–40 KB. The absence of a virtual DOM and direct access to browser APIs provide maximum performance, while development budget savings reach up to 40% (up to 50,000 RUB on a typical widget). Our team leverages modern language features: Web Components, IntersectionObserver, Proxy, and ESModules to build fast and reliable solutions. Every project starts with metric auditing and selecting the optimal architecture. Contact us for a project audit.
When Vanilla JS Is the Only Right Choice
Vanilla JS is justified in scenarios where frameworks add unnecessary complexity. Embed widgets are inserted into third-party sites — they must not conflict with the host's frameworks. Libraries on npm with zero dependencies do not bloat users' projects built with different stacks. High-load animations use Canvas API, WebGL, and Web Animations API directly, without a virtual DOM layer. Browser extensions run in the isolated environment of Content Scripts, where frameworks increase risks. Static sites with minimal interactivity do not need 50 KB for a single dropdown. A typical Vanilla JS widget costs 30-50% less than its React counterpart.
| Scenario | Vanilla JS | React | Vue |
|---|---|---|---|
| Embed widget | optimal | overkill | overkill |
| SPA with forms | difficult | optimal | optimal |
| Animations | excellent | average | average |
In one project, we replaced a React cart widget with Vanilla JS. The bundle shrunk from 120 KB to 28 KB (a 77% reduction), LCP dropped from 2.4 s to 0.8 s (67% faster). CPU load decreased by 40%. Vanilla JS loads twice as fast as React on slow connections, and development savings amounted to about 40,000 RUB.
Why Vanilla JS Outperforms Frameworks
The absence of a virtual DOM eliminates overhead from diffing and re-rendering. Direct work with document.createElement and appendChild allows manual control over every update. Combined with IntersectionObserver for lazy loading and requestAnimationFrame for animations, you get LCP under 1 second even on mobile devices. We also use preload for critical resources and prefetch for subsequent pages. Bundle reduction of 77% and development budget savings up to 40% are common when migrating to pure JS.
| Characteristic | Vanilla JS | React (typical project) |
|---|---|---|
| Bundle size | 20–40 KB | 80–150 KB (with React DOM) |
| Control | full | limited by virtual DOM |
| Dependencies | zero | React + ecosystem |
| Compatibility | any site | may conflict |
| Performance (LCP) | < 1 s | 1–2 s (average) |
What Modern Vanilla JS Can Do
Browser APIs have expanded significantly: now you have Custom Elements, Shadow DOM, AbortController, Proxy, and Service Workers. Example fetch with 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 — a component model without a 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} RUB`;
}
#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);
MDN: Web ComponentsUsage in HTML: <product-card product-id="42"></product-card>. Works in any framework or none.
Reactive state management with Proxy: a store without Redux
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 modal.js lib/ store.js api.js utils.js pages/ catalog.js product.js app.js {
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"vite": "^5.0.0"
}
}Zero runtime dependencies. Vite is used only for development and building.
How We Work on Vanilla JS Projects
- Analytics — identify critical scenarios, measure metrics (LCP, TTFB).
- Design — module architecture, component map, API contracts.
- Implementation — iterative development with code reviews and tests (Vitest + jsdom).
- Optimization — bundle splitting, prefetch, lazy loading, tree-shake.
- Deployment — build with Vite, deploy to CDN or hosting.
What is included in the work
- Source code in ESModules with comments.
- Component documentation and deployment instructions.
- 3-month support guarantee after delivery.
- Performance metrics before and after.
Estimated timelines
- Simple widget (up to 5 components) — from 2 to 5 days.
- Medium project (up to 20 components) — from 1 to 3 weeks.
- Complex application (up to 50 components) — from 4 to 8 weeks.
Pricing is determined individually. We will assess your project — write to us. Discuss your project — book a consultation. Additional budget savings of up to 50,000 RUB are possible when migrating from frameworks.







