DEV Community

Cover image for Fetching data with web workers
artydev
artydev

Posted on

Fetching data with web workers

If you want a good UX webworkers are your friend.
Look at this code

worker.js:

// worker.js
self.onmessage = async (event) => {
    const { action, count } = event.data;

    if (action === 'fetchUsers') {
        try {
            // Fetch a large batch of users
            const response = await fetch(`https://randomuser.me/api/?results=${count}`);

            if (!response.ok) throw new Error('API fetch failed');

            const data = await response.json();

            // Transform data here to keep the main thread fast
            // We only send the properties we actually need
            const processedUsers = data.results.map(user => ({
                id: user.login.uuid,
                name: `${user.name.first} ${user.name.last}`,
                email: user.email,
                picture: user.picture.large, // Large image for quality
                location: `${user.location.city}, ${user.location.country}`
            }));

            // Send back the clean data
            self.postMessage({ status: 'success', users: processedUsers });

        } catch (error) {
            self.postMessage({ status: 'error', message: error.message });
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Optimized User Fetcher</title>
    <style>
        :root {
            --card-bg: #ffffff;
            --bg-color: #f0f2f5;
            --text-main: #1c1e21;
        }

        body { 
            font-family: 'Inter', system-ui, sans-serif; 
            background-color: var(--bg-color); 
            margin: 0; padding: 20px; color: var(--text-main);
        }

        header { text-align: center; margin-bottom: 30px; }

        .controls { text-align: center; margin-bottom: 40px; }

        button {
            background: #007bff; color: white; border: none;
            padding: 12px 24px; border-radius: 8px; font-size: 16px;
            cursor: pointer; transition: background 0.2s;
        }

        button:hover { background: #0056b3; }

        /* The Grid */
        #user-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 20px; max-width: 1200px; margin: 0 auto;
        }

        /* The Card */
        .user-card {
            background: var(--card-bg);
            border-radius: 12px;
            padding: 20px;
            text-align: center;
            box-shadow: 0 4px 6px rgba(0,0,0,0.05);
            transition: transform 0.2s;
        }

        .user-card:hover { transform: translateY(-5px); }

        /* Image Optimization Styles */
        .img-container {
            width: 200px; height: 200px;
            margin: 0 auto 15px;
            border-radius: 50%;
            background: #e1e4e8; /* Skeleton placeholder color */
            overflow: hidden;
        }

        .user-card img {
            width: 100%; height: 100%;
            object-fit: cover;
            opacity: 0; /* Hidden initially for fade-in */
            transition: opacity 0.6s ease-in-out;
        }

        .user-card img.loaded {
            opacity: 1; /* Fade in once loaded */
        }

        .user-card h3 { margin: 10px 0 5px; font-size: 1.2rem; }
        .user-card p { color: #65676b; font-size: 0.9rem; margin: 4px 0; }

        #status { font-weight: bold; margin-left: 10px; color: #007bff; }
    </style>
</head>
<body>

    <header>
        <h1>Network Optimized User Directory</h1>
        <p>Using Web Workers + Native Lazy Loading + Intersection Observer</p>
    </header>

    <div class="controls">
        <button id="fetchBtn">Fetch 100 Users</button>
        <span id="status"></span>
    </div>

    <div id="user-grid"></div>

    <script>
        const fetchBtn = document.getElementById('fetchBtn');
        const userGrid = document.getElementById('user-grid');
        const status = document.getElementById('status');

        // 1. Initialize Web Worker
        const worker = new Worker('worker.js');

        fetchBtn.addEventListener('click', () => {
            status.innerText = "Worker fetching...";
            userGrid.innerHTML = ""; // Clear existing

            // 2. Request 100 users from the worker
            worker.postMessage({ action: 'fetchUsers', count: 300 });
        });

        // 3. Handle data returned from Worker
        worker.onmessage = (event) => {
            const { status: resStatus, users, message } = event.data;

            if (resStatus === 'success') {
                status.innerText = "Complete!";
                renderUsers(users);
            } else {
                status.innerText = "Error: " + message;
            }
        };

        function renderUsers(users) {
            const fragment = document.createDocumentFragment();

            users.forEach(user => {
                const card = document.createElement('div');
                card.className = 'user-card';

                // Note: loading="lazy" tells the browser to only fetch the image 
                // when it's near the viewport.
                card.innerHTML = `
                    <div class="img-container">
                        <img 
                            src="${user.picture}" 
                            alt="${user.name}" 
                            loading="lazy"
                            class="lazy-img"
                        >
                    </div>
                    <h3>${user.name}</h3>
                    <p>${user.email}</p>
                    <p><strong>${user.location}</strong></p>
                `;
                fragment.appendChild(card);
            });

            userGrid.appendChild(fragment);

            // 4. Setup Intersection Observer for the "Fade-in" effect
            initFadeInObserver();
        }

        function initFadeInObserver() {
            const images = document.querySelectorAll('.lazy-img');

            const observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        const img = entry.target;
                        // Add 'loaded' class when image is visible
                        img.onload = () => img.classList.add('loaded');
                        // If cached, it might already be loaded
                        if (img.complete) img.dispatchEvent(new Event('load'));
                        observer.unobserve(img);
                    }
                });
            }, { threshold: 0.1 });

            images.forEach(img => observer.observe(img));
        }
        (function () { fetchBtn.click() })(); // Auto-fetch on load
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)