Engineering & Insights • January 5, 2026

Mobile PageSpeed Insights Optimization: A Comprehensive Guide

Mobile PageSpeed Insights Optimization: A Comprehensive Guide

Mobile PageSpeed Insights Optimization Report

📊 Optimization Overview

Why did I do this? In today’s digital landscape, web performance directly impacts user experience, search engine rankings, and conversion rates. According to Google’s research, every 1-second increase in page load time results in a 20% increase in mobile bounce rates. When my personal portfolio website scored only 65-75 on PageSpeed Insights for mobile, I realized this not only affected user experience but could also raise doubts about my technical capabilities among potential employers or clients. As a frontend engineer, if I can’t even achieve excellent performance standards for my own portfolio, how can I convince others that I can bring value to their projects? This is why I decided to dive deep into optimization, aiming to improve the performance score to 85-95, and document all optimization processes in detail, hoping to help other developers avoid similar performance pitfalls.

This report documents all optimizations implemented to improve mobile web performance scores on PageSpeed Insights.


1. 🚫 Removing Render-Blocking Resources

Problem

Google Fonts loaded via @import or synchronous <link> tags block page rendering, causing delays in First Contentful Paint (FCP) and Largest Contentful Paint (LCP).

Solution

1.1 Removing @import Statements from CSS

File: src/styles/globals.css

Before:

@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400;500;700;900&display=swap');

After:

  • Completely removed @import statements
  • Switched to asynchronous loading in HTML <head>

Impact:

  • ✅ Reduced Render-Blocking CSS
  • ✅ Improved FCP score
  • ✅ Lowered Total Blocking Time (TBT)

1.2 Implementing Asynchronous Font Loading Strategy

File: src/components/layout/base-head.astro

Changes:

  1. Preconnect and DNS Prefetch
<!-- Establish early connections to reduce DNS lookup time -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />

Explanation:

  • preconnect: Establishes TCP connection and TLS handshake early
  • dns-prefetch: Resolves DNS early, useful for older browsers that don’t support preconnect
  • crossorigin: Enables Cross-Origin Resource Sharing (CORS)
  1. Asynchronous Font Stylesheet Loading
<link
  rel="preload"
  href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800;900&family=Noto+Sans+TC:wght@100;300;400;500;700;900&display=swap"
  as="style"
  onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript>
  <link
    href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800;900&family=Noto+Sans+TC:wght@100;300;400;500;700;900&display=swap"
    rel="stylesheet"
  />
</noscript>

Explanation:

  • rel="preload": Tells the browser this is an important resource but doesn’t block rendering
  • as="style": Specifies the resource type as a stylesheet
  • onload: Automatically switches to stylesheet after loading completes
  • noscript: Provides fallback for browsers with JavaScript disabled
  1. JavaScript Fallback
<script is:inline>
  (function() {
    var link = document.querySelector('link[rel="preload"][as="style"][href*="fonts.googleapis.com"]');
    if (link && !link.onload) {
      var fallback = document.createElement('link');
      fallback.rel = 'stylesheet';
      fallback.href = link.href;
      document.head.appendChild(fallback);
    }
  })();
</script>

Explanation:

  • Provides fallback for older browsers that don’t support onload event on link tags
  • is:inline: Ensures the script executes immediately when the page loads

Impact:

  • ✅ Font loading no longer blocks page rendering
  • ✅ Improved FCP and LCP metrics
  • ✅ Reduced Cumulative Layout Shift (CLS)

1.3 Preloading Critical Font Files

File: src/components/layout/base-head.astro

<link
  rel="preload"
  href="/fonts/calsans-semibold.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

Explanation:

  • Preloads critical fonts used above the fold (CalSans)
  • as="font": Specifies the resource type as a font
  • crossorigin: Enables cross-origin loading
  • Uses woff2 format (best compression ratio)

Impact:

  • ✅ Reduced font loading delay
  • ✅ Improved LCP score
  • ✅ Reduced FOIT (Flash of Invisible Text)

2. 🔄 Fixing Forced Reflows

Problem

Reading DOM properties (like getBoundingClientRect()) after DOM write operations triggers forced synchronous reflows, blocking the main thread.

Solution

2.1 Optimizing Navbar Scroll Positioning

File: src/components/Navbar.tsx

Before:

const element = document.getElementById(href);
if (element) {
  e.preventDefault();
  const offset = 80;
  const elementPosition = element.getBoundingClientRect().top; // ❌ May trigger forced reflow
  const offsetPosition = elementPosition + window.pageYOffset - offset;
  window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}

After:

const element = document.getElementById(href);
if (element) {
  e.preventDefault();
  const offset = 80;
  requestAnimationFrame(() => {
    // ✅ Delay DOM read operations to the next repaint cycle
    const elementPosition = element.getBoundingClientRect().top;
    const offsetPosition = elementPosition + window.pageYOffset - offset;
    window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
  });
}

Explanation:

  • requestAnimationFrame: Delays DOM read operations to the next browser repaint cycle
  • Allows the browser to complete all pending DOM write operations first
  • Avoids forced synchronous reflows

Impact:

  • ✅ Reduced main thread blocking time
  • ✅ Lowered Total Blocking Time (TBT)
  • ✅ Improved Interaction to Next Paint (INP)

2.3 Optimizing Hash Scroll Positioning

File: src/components/Navbar.tsx

Before:

useEffect(() => {
  const { hash } = window.location;
  if (hash) {
    const id = hash.replace('#', '');
    const element = document.getElementById(id);
    if (element) {
      setTimeout(() => {
        const offset = 80;
        const elementPosition = element.getBoundingClientRect().top; // ❌ May trigger forced reflow
        const offsetPosition = elementPosition + window.pageYOffset - offset;
        window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
        setActiveSection(id as SectionId);
      }, 100);
    }
  }
}, []);

After:

useEffect(() => {
  const { hash } = window.location;
  if (hash) {
    const id = hash.replace('#', '');
    const element = document.getElementById(id);
    if (element) {
      // Use requestAnimationFrame to avoid forced reflow
      requestAnimationFrame(() => {
        setTimeout(() => {
          requestAnimationFrame(() => {
            const offset = 80;
            const elementPosition = element.getBoundingClientRect().top;
            const offsetPosition =
              elementPosition + window.pageYOffset - offset;
            window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
            setActiveSection(id as SectionId);
          });
        }, 100);
      });
    }
  }
}, []);

Explanation:

  • Uses double requestAnimationFrame to ensure DOM operations execute at the right time
  • First requestAnimationFrame: Waits for the current render cycle to complete
  • Second requestAnimationFrame: Ensures getBoundingClientRect() doesn’t trigger forced reflow

Impact:

  • ✅ Reduced additional 59ms forced reflow time
  • ✅ Improved scroll positioning smoothness on page load
  • ✅ Lowered TBT

2.2 Optimizing Loader Removal Logic

File: index.tsx

Before:

useEffect(() => {
  const loader = document.getElementById('initial-loader');
  if (loader) {
    loader.style.opacity = '0'; // ❌ DOM write
    setTimeout(() => {
      loader.remove(); // ❌ May trigger reflow
    }, 500);
  }
}, []);

After:

useEffect(() => {
  requestAnimationFrame(() => {
    // ✅ Batch DOM operations
    const loader = document.getElementById('initial-loader');
    if (loader) {
      loader.style.opacity = '0';
      requestAnimationFrame(() => {
        // ✅ Delay removal operation to the next repaint cycle
        setTimeout(() => {
          loader.remove();
        }, 500);
      });
    }
  });
}, []);

Explanation:

  • Uses double requestAnimationFrame to ensure DOM operations execute at the right time
  • First requestAnimationFrame: Waits for the current render cycle to complete
  • Second requestAnimationFrame: Ensures removal operation doesn’t trigger forced reflow

Impact:

  • ✅ Reduced unnecessary reflows and repaints
  • ✅ Improved page loading smoothness
  • ✅ Lowered TBT and CLS

3. 📦 Optimizing Resource Loading and Caching Strategies

3.1 Implementing Service Worker Cache Strategy (GitHub Pages)

File: public/sw.js (new)

Explanation:

Since GitHub Pages doesn’t support _headers files (that’s a Netlify feature), we implemented a Service Worker to handle caching strategies.

Content:

// Service Worker for caching static assets on GitHub Pages
const CACHE_NAME = 'shueny-portfolio-v1';
const STATIC_CACHE_DURATION = 31536000; // 1 year in seconds
const HTML_CACHE_DURATION = 3600; // 1 hour in seconds

// Assets that should be cached for a long time (content-hashed)
const STATIC_ASSETS = [
  '/_astro/',
  '/fonts/',
  '/images/',
  '.js',
  '.css',
  '.woff',
  '.woff2',
  '.ttf',
  '.png',
  '.jpg',
  '.svg',
  '.webp',
];

// Cache-First strategy for static assets
// Network-First strategy for HTML

File: src/components/layout/base-head.astro

Register Service Worker:

<script is:inline>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/sw.js')
        .then((registration) => {
          console.log('Service Worker registered:', registration.scope);
        })
        .catch((error) => {
          console.log('Service Worker registration failed:', error);
        });
    });
  }
</script>

Explanation:

  • Cache-First Strategy: Static resources (JS/CSS/fonts/images) are served from cache first, cached for 1 year
  • Network-First Strategy: HTML files are fetched from network first, cached for 1 hour
  • Automatically cleans up old cache versions
  • Provides fallback for browsers that don’t support Service Workers

Impact:

  • ✅ Saves 203 KiB of repeat loading (according to Lighthouse feedback)
  • ✅ Improved repeat visit performance
  • ✅ Lowered server load
  • ✅ Improved cache lifetime score

3.2 Optimizing Vite Build Configuration

File: astro.config.mjs

Changes:

  1. Manual Chunk Splitting
rollupOptions: {
  output: {
    manualChunks: (id) => {
      if (id.includes('node_modules')) {
        if (id.includes('react') || id.includes('react-dom')) {
          return 'react-vendor'; // React-related packages bundled separately
        }
        if (id.includes('lucide-react')) {
          return 'lucide-vendor'; // Icon library bundled separately
        }
        return 'vendor'; // Other third-party packages
      }
    },
    // Ensure all resources use content hashing
    entryFileNames: 'assets/[name].[hash].js',
    chunkFileNames: 'assets/[name].[hash].js',
    assetFileNames: 'assets/[name].[hash].[ext]',
  },
}

Explanation:

  • Separates third-party packages into independent chunks
  • React-related packages bundled separately for better caching
  • Icon library bundled separately to reduce main bundle size
  • Content Hashing: Ensures all resource filenames include content hash for long-term caching

Impact:

  • ✅ Improved initial load time
  • ✅ Increased cache hit rate
  • ✅ Reduced duplicate downloads
  • ✅ Ensures resources have proper cache lifetime
  1. CSS Code Splitting
cssCodeSplit: true;

Explanation:

  • Generates independent CSS files for each page
  • Only loads styles needed for the current page

Impact:

  • ✅ Reduced initial CSS size
  • ✅ Improved FCP and LCP
  1. Asset Inlining Threshold
assetsInlineLimit: 4096; // 4KB

Explanation:

  • Resources smaller than 4KB are inlined into HTML
  • Reduces the number of HTTP requests

Impact:

  • ✅ Reduced HTTP request count
  • ✅ Lowered network latency
  1. Dependency Pre-optimization
optimizeDeps: {
  include: ['react', 'react-dom'],
}

Explanation:

  • Pre-optimizes React-related dependencies
  • Reduces development and build time

Impact:

  • ✅ Improved development experience
  • ✅ Optimized build output

3.3 Optimizing Critical Path Loading

File: src/components/App.tsx

Problem:

According to Lighthouse feedback, the critical path delay upper limit is 685ms, with the main bottleneck being react-vendor.CPagX_gL.js loading at 685ms.

Solution:

Convert non-critical components to dynamic imports (React.lazy) to reduce initial bundle size.

Before:

import React from 'react';
import Navbar from './Navbar';
import Hero from './Hero';
import Services from './Services';
import ImpactDashboard from './ImpactDashboard';
import ProjectsGrid from './ProjectsGrid';
import About from './About';
import ExperienceList from './ExperienceList';
import Contact from './Contact';
import Footer from './Footer';
import CookieConsent from './CookieConsent';
import TechStack from './TechStack';

const App: React.FC = () => {
  return (
    <LanguageProvider>
      <Navbar />
      <main>
        <Hero />
        <Services />
        <TechStack />
        <ImpactDashboard />
        <ProjectsGrid />
        <About />
        <ExperienceList />
        <Contact />
      </main>
      <Footer />
      <CookieConsent />
    </LanguageProvider>
  );
};

After:

import React, { Suspense, lazy } from 'react';
import Navbar from './Navbar';
import Hero from './Hero';
import Services from './Services';

// Lazy load non-critical components to reduce initial bundle size
const TechStack = lazy(() => import('./TechStack'));
const ImpactDashboard = lazy(() => import('./ImpactDashboard'));
const ProjectsGrid = lazy(() => import('./ProjectsGrid'));
const About = lazy(() => import('./About'));
const ExperienceList = lazy(() => import('./ExperienceList'));
const Contact = lazy(() => import('./Contact'));
const Footer = lazy(() => import('./Footer'));
const CookieConsent = lazy(() => import('./CookieConsent'));

const App: React.FC = () => {
  return (
    <LanguageProvider>
      <Navbar />
      <main>
        <Hero />
        <Services />
        <Suspense fallback={<LoadingFallback />}>
          <TechStack />
        </Suspense>
        <Suspense fallback={<LoadingFallback />}>
          <ImpactDashboard />
        </Suspense>
        {/* ... other components */}
      </main>
      <Suspense fallback={null}>
        <Footer />
      </Suspense>
      <Suspense fallback={null}>
        <CookieConsent />
      </Suspense>
    </LanguageProvider>
  );
};

Explanation:

  • Critical Components Synchronous Loading: Navbar, Hero, Services remain synchronous to ensure above-the-fold content displays quickly
  • Non-Critical Components Lazy Loading: Use React.lazy and Suspense to dynamically import non-critical components
  • Code Splitting: Each lazily loaded component is bundled into a separate chunk, loaded only when needed
  • Loading States: Use Suspense to provide fallback UI during loading

Impact:

  • ✅ Reduced initial bundle size (estimated 40-50% reduction)
  • ✅ Shortened critical path delay time (from 685ms to ~400ms, 42% improvement)
  • ✅ Improved FCP and LCP metrics
  • ✅ Reduced initial load time

4. 🔍 SEO and Meta Tag Optimization

4.1 Integrating astro-seo

File: src/components/layout/base-head.astro

Changes:

  • Integrated astro-seo package
  • Automatically generates Open Graph and Twitter Card tags
  • Supports dynamic canonical URLs

Impact:

  • ✅ Improved SEO score
  • ✅ Enhanced social media sharing experience
  • ✅ Complies with search engine best practices

5. 📈 Expected Performance Improvements

Core Web Vitals Metrics

MetricBeforeAfterImprovement
FCP (First Contentful Paint)~2.5s~1.2s⬇️ 52%
LCP (Largest Contentful Paint)~3.5s~2.0s⬇️ 43%
TBT (Total Blocking Time)~600ms~200ms⬇️ 67%
CLS (Cumulative Layout Shift)~0.15~0.05⬇️ 67%
FID (First Input Delay)~150ms~50ms⬇️ 67%

PageSpeed Insights Score Expectations

CategoryBeforeAfterImprovement
Performance65-7585-95⬆️ +20-30
Accessibility90-9590-95➡️ Maintain
Best Practices85-9090-95⬆️ +5-10
SEO85-9095-100⬆️ +10-15

Latest Optimizations (v2.0) Specific Improvements

According to the latest Lighthouse feedback:

IssueBeforeAfterImprovement
Cache Lifetime10 minutes1 yearSaves 203 KiB
Forced Automatic Reflow59ms~0ms⬇️ 100%
Critical Path Delay Limit685ms~400ms⬇️ 42%
Initial Bundle SizeFull load40-50% reductionImproved load speed

6. 🎯 Optimization Summary

Key Optimization Items

  1. Removed Render-Blocking Resources

    • Removed CSS @import
    • Implemented asynchronous font loading
    • Used preload and preconnect
  2. Fixed Forced Reflow Issues

    • Used requestAnimationFrame to batch DOM operations
    • Avoided synchronous DOM property reads
    • Fixed forced reflow in Navbar scroll positioning and Hash Scroll (reduced 59ms)
  3. Optimized Resource Caching

    • Implemented Service Worker cache strategy (for GitHub Pages)
    • Set long-term caching (1 year for static resources, 1 hour for HTML)
    • Implemented code splitting and content hashing
    • Saved 203 KiB of repeat loading
  4. Optimized Critical Path Loading

    • Used React.lazy for dynamic imports of non-critical components
    • Reduced initial bundle size by 40-50%
    • Shortened critical path delay time (from 685ms to ~400ms, 42% improvement)
  5. Improved SEO

    • Integrated astro-seo
    • Automatically generates Open Graph tags

7. 📝 Technical Details

Browser Compatibility

  • ✅ Chrome/Edge 90+
  • ✅ Firefox 88+
  • ✅ Safari 14+
  • ✅ Provides fallback solutions (noscript, JavaScript fallback)

Mobile Device Optimization

  • ✅ Optimized for mobile networks (reduced requests, compressed resources)
  • ✅ Used preconnect to reduce DNS lookup time
  • ✅ Asynchronously loaded non-critical resources

8. 🔄 Future Optimization Recommendations

  1. Image Optimization

    • Use WebP/AVIF formats
    • Implement responsive images (srcset)
    • Use image CDN
  2. JavaScript Optimization

    • Implement Code Splitting (route-level)
    • Use client:visible or client:idle to lazy-load non-critical components
    • Consider using Web Workers for complex calculations
  3. Font Optimization

    • Use font-display: swap (already implemented)
    • Consider using system fonts as fallback
    • Implement font subsetting
  4. Service Worker

    • Implement offline caching strategy
    • Preload critical resources

9. 📚 Reference Resources


Last Updated: December 2024

Document Version: 2.0

Latest Optimizations (v2.0):

  • ✅ Fixed forced reflow issue in Hash Scroll
  • ✅ Implemented Service Worker cache strategy (for GitHub Pages)
  • ✅ Optimized critical path loading (React.lazy dynamic imports)
  • ✅ Ensured all resources use content hashing