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 for free — write to us. Discuss your project — book a consultation. Additional budget savings of up to 50,000 RUB are possible when migrating from frameworks.







