placeholderBlog Performance Optimization Notes

Blog Performance Optimization Notes

Optimizations adopted while maintaining this site, from image compression to lazy loading and CDN.
/

Use Efficient Encodings

Encode assets in formats like webp and webm. Images can be compressed with the cwebp command; videos can be converted with online tools.

Recommended formats:

  • Images: webp, avif
  • Videos / animated images: webm
  • Fonts: woff2

Best Practice
While MP4 has been around since 1999, WebM is a relatively new file format initially released in 2010. WebM videos are much smaller than MP4 videos, but not all browsers support WebM so it makes sense to generate both.

Batch-Converting Images

Install the libwebp library to get the cwebp command. Below is the Python script I use for batch conversion.

The built-in Windows screenshot tool and QQ’s screenshot tool don’t support saving screenshots as webp;
recent versions of third-party tools like PixPin and Snipaste do.

from pathlib import Pathimport subprocessimport shutilimport argparsecovert_these_types = (".png", ".jpg", ".jpeg")def convert_to_webp(input_dir: Path, output_dir: Path):    """    Convert files in the given directory (and subdirectories) to .webp.    Output files are saved under the output directory, preserving the directory structure.    """    for file_path in input_dir.rglob("*"):        # Check whether the file is one of the types to convert        if file_path.suffix.lower() in covert_these_types:            # Build the output path; the / operator joins output_dir and relative_path into a new path object            relative_path = file_path.relative_to(input_dir)            output_path = output_dir / relative_path.with_suffix(".webp")            output_path.parent.mkdir(parents=True, exist_ok=True)            # Convert with the cwebp command            try:                subprocess.run(                    ["cwebp", str(file_path), "-o", str(output_path)], check=True                )            except subprocess.CalledProcessError as e:                print(f"Failed to convert {file_path}: {e}")        else:            output_path = output_dir / relative_path            shutil.move(file_path, output_dir)def main():    # Create the command-line argument parser    parser = argparse.ArgumentParser(        description="Convert PNG, JPG, and JPEG files to WEBP format."    )    parser.add_argument(        "input_dir", type=str, help="Input directory containing images to convert"    )    parser.add_argument(        "output_dir", type=str, help="Output directory to save converted images"    )    args = parser.parse_args()    input_dir = Path(args.input_dir)    output_dir = Path(args.output_dir)    convert_to_webp(input_dir, output_dir)if __name__ == "__main__":    main()
[USAGE]python ./cwebp.py ../source/img/unused/ ./output

web.dev has an article recommending replacing GIFs with video formats for better performance: Replace GIFs with video

ffmpeg -i input.gif -c vp9 -b:v 0 -crf 41 output.webm

Results

Here is a before/after size comparison of this site’s images:

╭───┬───────────────────┬──────┬───────────╮ #        name         type    size    ├───┼───────────────────┼──────┼───────────┤ 0  gallery            dir    66.2 MiB  1  gallery_origin     dir   186.8 MiB  2  thumbnails         dir    19.6 MiB  3  thumbnails_origin  dir    88.0 MiB  4  unused             dir    18.4 MiB ╰───┴───────────────────┴──────┴───────────╯
  • The gallery directory shrank by about 64.57%
    • Original size: gallery_origin: 186.8 MiB; 54 images (jpg, png), 3 videos (mp4)
    • Compressed size: gallery: 66.2 MiB
    • Saved: 186.8 MiB - 66.2 MiB = 120.6 MiB
  • The thumbnails directory shrank by about 77.73%
    • Original size: thumbnails_origin: 88.0 MiB; 100 images (jpg, png)
    • Compressed size: thumbnails: 19.6 MiB
    • Saved: 88.0 MiB - 19.6 MiB = 68.4 MiB

Minify HTML/CSS/JS

See another post of mine. Consider using wilsonzlin/minify-html and ESBuild to minify HTML/CSS/JS assets.

Lazy-Loading Images

npm install hexo-native-lazy-load --save

lazy_load:  enable: true  onlypost: false

Run hexo clean, deploy, then open devtools on the page; you’ll see that img elements now carry loading="lazy".

devtool demo
devtool demo

Instant Page (Migrated Away)

instant.page uses just-in-time preloading — it preloads a page right before a user clicks on it.

Usage: add a scripts/instant-page.js file in the root directory and register a hexo injector.

scripts/instant-page.js
hexo.extend.injector.register(  "body_end",  '<script src="//instant.page/5.2.0" type="module" integrity="sha384-jnZyxPjiipYXnSU0ygqeac2q7CVYMbh84q0uHVRRxEtvFPiQYbXWUorga2aqZJ0z"></script>',  "default",);

Update, 2026-06: this site has replaced instant.page with the browser-native Speculation Rules API. For why I migrated, how to write the rules, and how it interacts with Swup/PJAX, see Replacing instant.page with the Speculation Rules API.

Pjax

Icarus currently has only preliminary Pjax support, see https://github.com/ppoffice/hexo-theme-icarus/pull/1287

OSS + CDN

A widely used free option: GitHub as an image host with jsDelivr as the CDN. It’s free, but access from mainland China is mediocre, and it violates their terms of service — it’s an abuse of public resources.

Cloudflare offers a free quota of R2 + CDN, which is worth considering; it requires a credit card, and binding a domestic UnionPay card through PayPal works. This is what this site currently uses.

To Be Continued(?)

Tools

Further Optimizations