Website Performance Optimization: From 53 to 97 on PageSpeed Insights

本文同时提供以下语言的翻译: 中文.

Introduction

Recently, in order to improve the user experience of my site, I decided to conduct a targeted performance optimization using PageSpeed Insights analysis.

Initially, I optimized the site based on the Cactus theme, raising the score from 47 to 94. During the writing of this post, I switched the theme to Icarus, which caused some previous optimizations to become invalid. The mobile score plummeted to 53 overnight. I had to start over, but by applying the same methodology, I eventually pushed the PageSpeed Insights score to 97.

Initial Status Analysis

PageSpeed Insights Score 53 Before Optimization

The PageSpeed Insights report highlighted the following major issues:

Metric Initial Value Issue
First Contentful Paint (FCP) 7.1s Too Slow
Largest Contentful Paint (LCP) 15.0s Severely Slow
Cumulative Layout Shift (CLS) 0.339 Significant Shift
Speed Index 5.2s Poor Experience

Note: Metrics varied during different test runs, but the core issues remained the same.

Key performance bottlenecks included:

  1. Oversized Font Files: MesloLG font in TTF format was as large as 636KB.
  2. Font Awesome CSS: Loaded the full icon library (~19KB) even though only a few icons were used.
  3. Missing Preconnect: No preconnect hints for critical resources like CDN.
  4. Unoptimized Images: The Logo image was 447x432 but displayed at 88x88.
  5. Layout Shift: Images lacked width/height attributes, causing CLS issues.

Round 1: Resource Slimming

1. Font Format Conversion (TTF → WOFF2)

WOFF2 is currently the most efficient Web font format, offering compression rates far superior to TTF.

1
2
# Convert font using ttf2woff2
npx ttf2woff2 < MesloLGS-Regular.ttf > MesloLGS-Regular.woff2

Result: 636KB → 160KB, a 75% reduction.

Then update the font reference in CSS to prioritize WOFF2:

1
2
3
4
5
6
7
@font-face
font-style: normal
font-family: "Meslo LG"
font-display: swap
src: local("Meslo LG S"),
url("../lib/meslo-LG/MesloLGS-Regular.woff2") format("woff2"),
url("../lib/meslo-LG/MesloLGS-Regular.ttf") format("truetype")

2. Add Preconnect Hints

Add resource preconnects in the <head> to allow the browser to establish TCP connections in advance:

1
2
3
4
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
<link rel="preconnect" href="https://pagead2.googlesyndication.com" crossorigin>
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>

3. Logo Optimization

The original Logo was 447x432 pixels, but only needed 88x88 for display.

sips (Scriptable Image Processing System) is a built-in command-line image processing tool on macOS. You can perform image resizing and format conversion without installing any third-party software.

1
2
# Resize image using sips
sips -z 88 88 Logo_Sketch.png --out Logo_Sketch_88.png

Note: If you are using Linux or need more complex image processing, you can use ImageMagick’s convert command as an alternative.

Result: 32KB → 7.6KB, a 76% reduction.

4. Fixing CLS Layout Shift

Add explicit dimension attributes to the Logo image:

1
<img id="logo" src="/images/Logo_Sketch_88.png" width="50" height="50" />

Score 61 After Round 1

Score after Round 1: 53 → 61


Round 2: Removing Font Awesome

Analysis showed that the Font Awesome CSS was about 19KB, plus even larger font files, but I only used the following icons:

  • ☰ Menu Icon (fa-bars)
  • ‹ › Pagination Arrows (fa-angle-left/right)
  • Social Media Icons (GitHub, YouTube, Bilibili, etc.)

Solution: Replace with SVG

1. Menu Icon

1
2
3
4
5
6
7
8
9
10
<!-- Before -->
<i class="fa-solid fa-bars fa-2x"></i>

<!-- After -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>

2. Pagination Arrows

Use Unicode characters directly:

1
2
3
4
5
<!-- Before -->
<i class="fa-solid fa-angle-left"></i>

<!-- After -->

3. Social Media Icons

Create an SVG icon mapping object:

1
2
3
4
5
6
var svgIcons = {
github: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31..."/></svg>',
youtube: '<svg ...>...</svg>',
bilibili: '<svg ...>...</svg>',
// ...
};

Then remove the Font Awesome CSS reference:

1
2
<!-- Remove this line -->
<link rel="stylesheet" href="/lib/font-awesome/css/all.min.css" />

Score 94 After SVG Optimization

Score after Round 2: 61 → 94 (Mobile), 100 (Desktop)

Why is removing Font Awesome so effective?

The core logic lies in the collapse of the “Critical Request Chain.”

Before: The browser had to go through a 4-level waterfall: HTML → style.css → all.min.css → fa-solid-900.woff2. Each level requires a full RTT (Round Trip Time). On a slow mobile network, this is how 2.4s of latency accumulates.

After: SVGs are inlined directly in the HTML byte stream. When the browser parses the icon’s position, the drawing instructions are already ready. The request chain length drops from 4 to 0.

When I opened the browser’s Network panel and saw all.min.css slowly performing its handshake, subsequently triggering the download of two 100+KB font files, I couldn’t sit still. The entire page was left blank for 2 seconds just for those few pagination arrows and menu icons.

Why I finally decided to “ax” Font Awesome:
In Android development, we would never integrate a multi-MB SDK just to display three icons. But in Web development, including all.min.css seems to have become a “default operation.”


While pursuing performance, I discovered an overlooked SEO issue: dead links in the source code.

Many Hexo themes (including Icarus) have a pagination logic that works like this:

  • On the first page, the “Previous” button is set to is-invisible (hidden visually) via CSS, but the HTML source still generates <a href="/page/0/">Previous</a>.
  • Search engine crawlers (like Googlebot) ignore CSS styles and follow the href. Since /page/0/ doesn’t exist, this leads to numerous 404 errors, dragging down the site’s authority.

Solution: Localized Component Rewrite

I extracted the paginator.jsx component locally and modified the rendering logic:

1
2
3
4
5
6
7
8
9
// Before: Generates <a> regardless
<div class="pagination-previous">
<a href={getPageUrl(current - 1)}>{prevTitle}</a>
</div>

// After: Renders as <span> when there is no valid page, eliminating the link physically
<div class={`pagination-previous${current > 1 ? '' : ' is-invisible'}`}>
{current > 1 ? <a href={getPageUrl(current - 1)}>{prevTitle}</a> : <span>{prevTitle}</span>}
</div>

2. Remove Blocking Progress Bar (Pace.js)

The report showed pace.min.js as a long chain request. Although it shows a page loading progress bar:

  • It is located in the <head>, making it a blocking resource.
  • It does nothing to help LCP (Largest Contentful Paint) and instead consumes network bandwidth.

Operation: Disable it directly in _config.icarus.yml.


Round 4: Modern Image Formats & Adaptive Sizing

After solving code and font issues, the only remaining “heavyweight” in the PageSpeed Insights suggestions was Image Resources.

The report pointed out two main problems:

  1. Oversized Images: For example, a 1536px wide cover image displaying at ~360px on mobile, wasting huge amounts of bandwidth and slowing down LCP.
  2. Legacy Formats: Still using JPG/PNG instead of WebP, which offers far better compression.

Solution: Automated WebP Conversion & Resizing

To solve this fundamentally, I wrote an automated script based on the Node.js sharp library to clean up main images across the site:

1
2
3
4
5
6
7
// Script logic summary: Automated batch processing
const sharp = require('sharp');
// ...
await sharp(src)
.resize({ width: 600 }) // Limit width to 600px (article container width)
.webp({ quality: 80 }) // Convert to WebP
.toFile(dest);

Specific Optimization Cases

  1. Ditherpunk Demo Image (returnofobradinn)

    • Before: 533KB (JPG)
    • After: ~26KB (WebP, 672px)
    • Reduction: ~95%. This was decisive for the LCP improvement.
  2. Post Cover Image (esp32_dither_cover)

    • Before: 1536x1587 resolution (533KB)
    • After: 672x694 resolution (~28KB)
    • Adaptive Sizing: Strictly matched container width (672px), eliminating “Oversized Image” warnings.
  3. M5Stack Cardputer Cover (cardputer-streaming-cover)

    • Before: 1200x900 resolution
    • After: 600x450 resolution (~20KB)
    • For pages with smaller display areas, the width was further limited to 600px to match the actual rendering size (599px).
  4. Sidebar Thumbnails

    • Before: ~20KB (original image shrunk)
    • After: ~3KB (WebP, 120px)
    • Generated specialized 120px micro-versions to avoid loading large images for small icons.
  5. Site Logo

    • Before: PNG format (32KB)
    • After: WebP format (8.1KB) with transparent background support.

Further Squeeze: Adjusting Compression Factor

While converting to WebP helped, the default quality of 80 was still overkill for some large images. PageSpeed Insights suggested “Efficiently encoding images,” so I re-processed them with higher compression:

1
2
3
4
5
6
7
// Using sharp's extreme compression parameters
.webp({
quality: 65, // Lower quality to 65
alphaQuality: 65, // Also compress the Alpha channel
effort: 6, // Enable highest level of compression (uses more CPU for smaller size)
smartSubsample: true
})

These small optimizations added up, eventually maxing out the LCP score. Total image volume across the site was reduced by over 1MB.


Round 5: Eliminating Critical Request Chain Blocking

Even after optimizing images and fonts, PageSpeed Insights still flagged a 441ms “Critical Request Chain” block, caused by CSS requests to fonts.googleapis.com and subsequent font downloads.

Although we introduced local WOFF2 fonts in Round 1, the Hexo theme (layout/common/head.jsx) still loaded Google Fonts (Ubuntu, Source Code Pro) by default.

Actions

  1. Remove Google Fonts: Commented out the Google Fonts loading code in the theme. Since we already configured a local font stack (Meslo LG / System Fonts), this was safe and immediately saved 441ms of blocking time.
  2. Async Load Non-Critical CSS: For code highlighting styles (atom-one-light.css), which don’t affect initial content reading (LCP elements are usually titles or cover images), I switched to asynchronous loading:
1
2
3
4
5
6
<!-- Before: Render-blocking -->
<link rel="stylesheet" href="/css/atom-one-light.css">

<!-- After: Explicit preload + Async application -->
<link rel="preload" as="style" href="..." onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="..."></noscript>

After these “surgeries,” the critical request chain depth dropped from 5 to 2, and FCP (First Contentful Paint) was significantly reduced.


Conclusion

While writing this article, I initially used the hexo-cactus-theme. During the process, I switched to hexo-icarus-theme. When I first switched to Icarus, the score dropped below 60. However, by applying the optimization principles described here—even with a home page that displays images instead of pure text like Cactus—the score eventually reached 97.

Optimization Original (Icarus Default) Optimized Reduction
MesloLG Font 636KB (TTF) 160KB (WOFF2) 75%
Logo Image 32KB 8.1KB 74%
Image Resources JPG/PNG (Oversized) WebP (Adaptive) > 90%
Font Awesome ~19KB + Font 0KB (Inline SVG) 100%
Google Fonts 441ms Block 0ms (Removed) 100%
Preconnect hints None 4 preconnects -
CLS Score 0.339 ~0 -
PageSpeed Score Icarus Initial Optimized
Mobile 53 97
Desktop 67 100

Final Score 97

Lessons Learned

  1. Fonts are a major factor: Web fonts are often overlooked performance killers; WOFF2 is the preferred format.
  2. Use icon libraries sparingly: If you only use a few icons, inlined SVG is much more efficient than loading a whole library.
  3. Images on demand: Ensure image dimensions match their display dimensions.
  4. Preconnect works: For necessary third-party resources, preconnect can save DNS + TCP time.
  5. CLS is easy to fix: Giving images explicit dimension attributes can avoid most layout shifts.
  6. Cache Lifecycle (TTL): Beyond resource size, use a CDN like Cloudflare to force long-term caching for static assets (Cache-Control: max-age=31536000). This ensures that when a user visits a second article, fonts and CSS load instantly from the Disk Cache.

Hope this article helps you! If you are also optimizing your website’s performance, feel free to reach out and exchange ideas.

Website Performance Optimization: From 53 to 97 on PageSpeed Insights

https://chaosgoo.com/en/pagespeed-optimization-journey/

Author

Chaos Goo

Posted on

2026-02-13

Updated on

2026-02-13

Licensed under