DEV Community

dev-web-hub
dev-web-hub

Posted on

I Built a Mobile Puzzle Game in Vanilla JS — No Framework, No Build Step

I Built a Mobile Puzzle Game in Vanilla JS — No Framework, No Build Step

When clients approach us at CodeBuddy.tech asking about mobile game development, they often assume we need React Native, Unity, or some heavyweight framework. Recently, I decided to challenge that assumption by building a complete mobile puzzle game using nothing but vanilla JavaScript, HTML5, and CSS3. The results surprised even me.

The game runs smoothly on mobile devices, has touch controls, persistent scoring, and weighs in at under 50KB total. No npm install, no webpack, no build pipeline — just pure web technologies doing what they do best.

Why Choose Vanilla JavaScript for Mobile Games?

Before diving into the technical details, let's address the elephant in the room. Why vanilla JS when frameworks exist to make development easier?

Performance is the primary reason. Mobile devices, especially budget Android phones, have limited processing power and memory. Every layer of abstraction costs performance. When you're targeting 60fps on a device that might have 2GB of RAM, every millisecond matters.

The second reason is deployment simplicity. With vanilla JavaScript, your entire game can be hosted on any web server. No special deployment pipelines, no server-side rendering concerns, no version conflicts. Upload your files and you're live.

Finally, there's the learning factor. Building with vanilla JS forces you to understand the underlying web platform. This knowledge makes you a better developer regardless of which frameworks you use later.

Setting Up the Mobile-First Architecture

Mobile-first design isn't just about responsive CSS — it's about thinking mobile from the ground up. I structured the game with these core modules:

  • Touch Controller — Handles all input events with proper touch debouncing
  • Game State Manager — Manages game logic and state transitions
  • Renderer — Handles all DOM manipulation and animations
  • Storage Manager — Manages localStorage for persistent data

The architecture uses a simple module pattern with IIFE (Immediately Invoked Function Expressions) to create namespaces without polluting the global scope:

const GameCore = (function() {
    let gameState = 'menu';
    let score = 0;
    
    function init() {
        TouchController.init();
        Renderer.init();
        StorageManager.loadGame();
    }
    
    return { init };
})();

This pattern gives you encapsulation without the overhead of a module bundler.

Implementing Smooth Touch Controls

Touch controls make or break mobile games. The key is handling the differences between mouse events and touch events while preventing the dreaded "double-tap zoom" and other mobile browser quirks.

I implemented a unified input system that normalizes touch and mouse events:

const TouchController = (function() {
    let startX, startY;
    
    function handleStart(e) {
        e.preventDefault();
        const touch = e.touches ? e.touches[0] : e;
        startX = touch.clientX;
        startY = touch.clientY;
    }
    
    function handleEnd(e) {
        e.preventDefault();
        const touch = e.changedTouches ? e.changedTouches[0] : e;
        const deltaX = touch.clientX - startX;
        const deltaY = touch.clientY - startY;
        
        processSwipe(deltaX, deltaY);
    }
    
    function init() {
        const gameArea = document.getElementById('game');
        
        // Touch events
        gameArea.addEventListener('touchstart', handleStart, { passive: false });
        gameArea.addEventListener('touchend', handleEnd, { passive: false });
        
        // Mouse events for desktop testing
        gameArea.addEventListener('mousedown', handleStart);
        gameArea.addEventListener('mouseup', handleEnd);
    }
    
    return { init };
})();

The { passive: false } option is crucial for preventing default browser behaviors like pull-to-refresh and pinch-to-zoom.

Optimizing Performance Without Frameworks

Performance optimization in vanilla JavaScript requires manual attention to details that frameworks often handle automatically. Here are the key techniques I used:

Request Animation Frame: All animations use requestAnimationFrame instead of setTimeout or setInterval. This ensures smooth 60fps performance and automatically pauses when the tab isn't visible.

Object Pooling: Instead of creating and destroying game objects constantly, I pre-allocate object pools and reuse them:

const ObjectPool = (function() {
    const pools = {
        particles: [],
        tiles: []
    };
    
    function get(type) {
        return pools[type].pop() || createNew(type);
    }
    
    function release(type, obj) {
        obj.reset();
        pools[type].push(obj);
    }
    
    return { get, release };
})();

Efficient DOM Updates: Batch DOM updates and use DocumentFragment for multiple insertions. Avoid reading layout properties immediately after writing them to prevent forced reflows.

CSS Hardware Acceleration: Strategic use of transform3d() and will-change properties moves animations to the GPU on mobile devices.

Managing Game State and Persistence

Without a state management library like Redux, you need to be disciplined about state management. I used a simple pub/sub pattern for state changes:

const EventBus = (function() {
    const events = {};
    
    function on(event, callback) {
        if (!events[event]) events[event] = [];
        events[event].push(callback);
    }
    
    function emit(event, data) {
        if (!events[event]) return;
        events[event].forEach(callback => callback(data));
    }
    
    return { on, emit };
})();

For persistence, localStorage provides everything needed for save games, high scores, and user preferences. The key is implementing proper error handling for when storage is unavailable or full.

Deployment and Distribution Advantages

The deployment story for vanilla JavaScript games is remarkably simple. The entire game consists of static files that can be served from any CDN or basic web hosting. No server requirements, no environment variables, no build step that could fail in production.

For PWA features, a simple service worker enables offline play and app-like installation on mobile devices. Users can add the game to their home screen and it behaves like a native app.

The small bundle size (under 50KB compressed) means instant loading even on slow mobile connections. This is crucial for user retention — every second of loading time costs users.

Results and Performance Metrics

The final game achieves consistent 60fps on devices as old as iPhone 6 and budget Android phones from 2019. Lighthouse scores are consistently above 95 for performance, accessibility, and best practices.

Load times average under 1 second on 3G connections. Memory usage stays below 50MB even during extended play sessions. Battery drain is minimal compared to equivalent native games.

User feedback has been overwhelmingly positive, with many players surprised to learn it's a web app rather than a native game.

Ready to Build Your Mobile Game?

Building mobile games with vanilla JavaScript proves that you don't always need heavyweight frameworks to create engaging experiences. Sometimes the simplest approach delivers the best results.

At CodeBuddy.tech, we specialize in building fast, efficient web applications and games using the right technology for each project — whether that's vanilla JavaScript, modern frameworks, or hybrid approaches. Our escrow-protected development process ensures you

Top comments (0)