Lazy image resizing in hexo-image-sizes v2
23 April 2018
I just released version 2 of
hexo-image-sizes
, which
represents a complete rewrite of the plugin. It finally supports lazy image
resizing. In this post, I will cover the high-level design ideas behind the new
version and some of the challenges in making it.
Check out my first post on it for more background on the functionality.
Lazy image resizing with imsize tags
hexo-image-sizes
uses a special tag format for adding images to posts in Hexo.
Users add an imsize
tag to their post when they want to use an image with the
plugin, and in the tag they can specify various details of the image including
its size profile, its alt text, its link target, and more.
Here’s an example use of imsize
within a markdown post:

When Hexo renders a post containing an imsize
tag, it invokes the tag’s
registered function. The tag function has access to the user’s arguments and
some context information including the current page on which it appears, which
will be important for resolving filenames.
The imsize
tag affords the plugin a special ability. Because users must invoke
the tag when they want to embed a resized image in their post,
hexo-image-sizes
can monitor exactly which images the user has actually made
visible in his or her posts. The new version of the plugin exploits this fact to
avoid resizing images that the user never makes visible on the site.
The plugin’s operation now comprises two distinct phases during static site generation:
- First, while Hexo renders each post on the site, the plugin monitors all the
imsize
tags the user includes in posts. Each time the user includes animsize
tag, the plugin records which image the user included and the desired new size in a cache in memory. - After Hexo has processed all of the posts in the site, the plugin knows which images the user wants to make visible. It generates resized versions of each visible image.
Using Hexo’s router to avoid filesystem manipulation
Hexo has a router module responsible for tracking which files are in use on the site and what their contents should be. The router is a map from the path of the file (relative to the Hexo site’s public output directory) to a stream of the contents of the file.
The documentation for the router is missing some important details. First, it’s very important to know that each route corresponds to a a file in the public directory and not in the source directory. Therefore, if you add a route, you will add a file to the public site but not alter the source directory. When I first began work on this plugin, I assumed that the routes referenced files in the source directory. I learned about using the router from a similar plugin, hexo-filter-responsive-images.
Using the router to add new resized images to the public output of the site generator has several advantages. By getting each file’s contents from the route stream managed by the router, my plugin can start to cooperate with other asset-management plugins, like image filters and minifiers. Using the router also allows me to remove the hacky filesystem code I was using before, where I had to guess the output location of each file and create the directory structure there.
Using the router also has a downside. I’m using
sharp to resize the images, and it takes as
input either a file or a Buffer of image data, while the router provides a
stream for each file. To run the image through sharp
, I have to read the full
file stream into memory (in a Buffer), which adds a lot to the memory use of the
plugin. In its original form, the plugin would invoke sharp with file pointers
instead of file contents, which meant that only sharp
would need to see the
full contents of the image, outside of the JavaScript VM. For my site, the
increased memory use has not been an issue.
Keeping Hexo running until all images are resized
I discovered that running Hexo in server mode with hexo server
can behave
differently than running it to generate files and quit with hexo generate
. My
site would generate without issue when running the server, but hexo generate
would only produce a few of the necessary images.
The problem was with my after_generate
filter function. Hexo uses the return
value of filters to determine when they are finished running, and my filter
wasn’t returning anything. When Hexo was running in server mode, it would keep
running indefinitely, long enough to allow my plugin to finish resizing
everything. In generate mode, it would exit as my plugin was just starting,
because my filter function seemed to run synchronously.
To fix the issue, I simply return a Promise from the filter function. The Promise resolves when every image has been resized.
Normalizing file paths throughout the application
Hexo uses several different types of file path, and mixing them was hindering my development of the plugin for a while. The following paths are involved in resizing an image:
- The path the user put in the
imsize
tag, which could be:- Absolute (with a leading slash), meaning it references the Hexo source directory
- Relative (no leading slash), meaning it is relative to the current post:
- If the current post is a blog post and has an asset directory
(
post_asset_folder
) is true in Hexo config, then the path starts in the asset directory - If the current post is not a blog post or
post_asset_folder
is false, then the path starts in the directory of the post file.
- If the current post is a blog post and has an asset directory
(
The plugin also has access to:
- The absolute path to Hexo’s source directory
- The absolute path to the current post’s source file
- The absolute path to the current post’s asset directory, if there is one
The plugin computes:
- A relative path from Hexo’s source directory to the image the user wants to
resize. For example, a cat picture might have path
/posts/2018-cats/cat1.jpg
- A copy of that relative path with the image renamed to have its profile name
as a prefix. For example, a thumbnail-sized version of the above cat picture
might have path
/posts/2018-cats/thumbnail-cat1.jpg
Once we know the Hexo-relative path to the input image and the output image, we have all the information necessary to resize the image.