Automating Skeleton Screens to Boost Perceived Page Load Speed

This article explains why users care about sub‑two‑second page loads, introduces skeleton screens as a visual solution, reviews existing front‑end approaches, and details a custom automated pipeline using Puppeteer and a draw‑page‑structure plugin to generate and inject skeleton screens with minimal maintenance.

Mafengwo Technology
Mafengwo Technology
Mafengwo Technology
Automating Skeleton Screens to Boost Perceived Page Load Speed

1. What is a Skeleton Screen

A skeleton screen displays a gray‑white layout of a page before real data arrives, giving users the impression that the page is progressively rendering and avoiding long white pages.

Advantages include preventing users from seeing a blank page, reducing bounce rates, and providing a smoother visual experience compared to traditional loading spinners.

2. Common Frontend Skeleton Screen Solutions

Typical methods are:

UI Skeleton Image : Insert a base64 image matching the page style into the root node. Simple but requires designer support and cannot be generated automatically.

Hand‑written Skeleton : Manually code HTML/CSS for each page. Accurate but costly to maintain when page styles change.

Automatic Static Skeletons : Tools like page‑skeleton‑webpack‑plugin use Puppeteer to capture a page, replace elements with gray blocks, and output HTML/CSS. Limitations include inaccurate timing, reliance on network‑idle events, and the need for manual post‑processing.

3. Developer‑Friendly Implementation

Our team built an automated solution inspired by draw‑page‑structure . The workflow:

Define target URLs and output paths in a configuration file.

Run a script that launches Puppeteer, opens each URL, and injects evalDom.js to traverse the DOM.

For each node, decide whether to ignore, render as a block, or apply custom handling based on customizeElement callbacks.

Generate absolute‑positioned div elements with calculated top, left, width, height, z-index, and background styles.

Write the resulting skeleton HTML into the project and inject it into the page root during build or at runtime.

Key configuration ( dpsConfig):

const dpsConfig = {
  // Example entry
  visa_guide: {
    url: 'https://w.mafengwo.cn/sfe-app/visa_guide.html?mdd_id=10083',
    // optional settings
    device: 'pc',
    background: '#eee',
    animation: 'opacity 1s linear infinite;',
    headless: false,
    customizeElement: function(node) {
      // return 0 to recurse, 1 to render only this node, 2 to skip
      if (node.className === 'navs-bottom-bar') { return 2; }
      return 0;
    },
    showInitiativeBtn: true,
    writePageStructure: function(html) {
      // e.g., fs.writeFileSync(filepath, html);
    },
    init: function() {
      // optional pre‑processing before rendering
    }
  }
};
module.exports = dpsConfig;

Puppeteer helper ( pp):

const puppeteer = require('puppeteer');
const { log, getAgrType } = require('./utils');
const insertBtn = require('../insertBtn');

const devices = {
  mobile: [375, 667, 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'],
  ipad: [1024, 1366, 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1'],
  pc: [1200, 1000, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36']
};

async function pp({device = 'mobile', headless = true, showInitiativeBtn = false}) {
  const browser = await puppeteer.launch({headless});
  async function openPage(url, extraHTTPHeaders) {
    const page = await browser.newPage();
    if (showInitiativeBtn) {
      browser.on('targetchanged', async () => {
        const targets = await browser.targets();
        const currentPage = await targets[targets.length - 1].page();
        setTimeout(() => { if (currentPage) currentPage.evaluate(insertBtn); }, 300);
      });
    }
    const [width, height, ua] = devices[device];
    await page.setUserAgent(ua);
    await page.setViewport({width, height});
    if (extraHTTPHeaders && getAgrType(extraHTTPHeaders) === 'object') {
      await page.setExtraHTTPHeaders(new Map(Object.entries(extraHTTPHeaders)));
    }
    await page.goto(url, {waitUntil: 'networkidle0'});
    return page;
  }
  return {browser, openPage};
}
module.exports = pp;

Core rendering logic in evalDom.js walks the DOM, checks for background images, text nodes, or custom rules, and creates style strings such as:

const styles = [
  'position: fixed',
  `z-index: ${zIndex}`,
  `top: ${top}%`,
  `left: ${left}%`,
  `width: ${width}%`,
  `height: ${height}%`,
  `background: ${background || '#eee'}`
];
if (radius && radius !== '0px') styles.push(`border-radius: ${radius}`);
blocks.push(`<div style="${styles.join(';')}"></div>`);

The final skeleton HTML is injected into the entry index.html (or via webpack) under a container with id skeleton, and can be removed programmatically once real content loads.

4. Summary

The presented pipeline enables developers to generate skeleton screens automatically for multiple pages, reduces manual effort, and improves perceived performance without sacrificing maintainability. Future work includes supporting custom node styles, component‑level skeletons, and refining the internal node‑filtering algorithm.

PuppeteerWebpackSkeleton Screen
Mafengwo Technology
Written by

Mafengwo Technology

External communication platform of the Mafengwo Technology team, regularly sharing articles on advanced tech practices, tech exchange events, and recruitment.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.