Home About Me

Three Practical Improvements That Made the Blog Faster and Easier to Browse

With some spare time recently, I made a series of visual and usability improvements to the blog. I was not deeply involved in the code changes themselves: I mainly described what I wanted, replaced the relevant files, and tested whether the final behavior matched expectations. The implementation choices and most of the coding were handled separately.

A quick overview

The work focused on three areas: pagination for the RSS-based friends page, thumbnail loading for the album, and collapsible sections on the archive page. Each change addressed a fairly specific problem, but together they made the site noticeably more responsive and easier to navigate.

1. Pagination for the friends page

The friends page collects posts from more than 20 RSS subscriptions. Previously, only the latest 30 items were retained because the cached data was cut off with array_slice($newrssFeeds, 0, 30). Even when the feeds contained hundreds of posts, there was no way to browse anything beyond those 30 entries.

There was also a more serious performance issue. RSS fetching ran through fastcgi_finish_request(), but the PHP session lock was still held while the background work continued. Opening another page during that period meant the request had to wait. In practice, the site could appear to freeze while the feeds were being updated.

The file lock had a similar problem. An exclusive flock(LOCK_EX) caused later visitors to wait whenever another request was updating the cache. At the same time, the fetch process handled around 50 sources serially, with a curl timeout of up to 10 seconds per source. In the worst case, a PHP-FPM process could remain occupied for roughly 500 seconds.

The revised implementation changed several parts of the process:

  • The cache now keeps every successfully fetched article instead of truncating the list at 30 items.
  • An AJAX endpoint was added in the form of ?action=loadmore&page=N. It returns 30 entries per request along with hasMore and total values.
  • The first 30 articles are rendered initially, with a “Load more” button for the rest. The interface handles loading, retrying after failure, and the end of the list, while also preventing repeated clicks from creating duplicate requests.
  • Before calling fastcgi_finish_request(), the code now runs session_write_close(), ob_end_flush(), and flush(), releasing the session lock before the background fetch begins.
  • The file lock uses LOCK_NB. If another process already owns the lock, the new request exits immediately instead of waiting in line.
  • The connection timeout was reduced to two seconds, the total request timeout to three seconds, and set_time_limit(30) was added to prevent an unusually slow update from occupying the process indefinitely.
  • Date formatting continues to use the theme’s shared getHumanizedDate() method. A temporary JavaScript-based date formatter was removed so that the page would remain consistent with the rest of the site.

The result is no longer limited to the newest 30 posts: the complete set of cached subscription articles can be browsed through successive loads. Switching to another page is also immediate instead of being blocked by the RSS update. The existing non-blocking asynchronous update process, file locking, and support for both RSS 2.0 and Atom were retained.

There was one predictable issue during testing. The first attempt at loading more posts immediately reported that there was nothing else to load. The cause was not the pagination endpoint but the old cache, which still contained only 30 entries. After deleting outrss.json and fetching the feeds again, pagination worked normally.

2. Smaller, faster album images

The album had a simpler but more visible bottleneck: its image tags pointed directly to the original files, some of which were more than ten megabytes. Lazy loading was already present through data-original, but it only delayed loading the original image. It did not reduce the amount of data eventually transferred, so the page could still become extremely slow.

There was no thumbnail-generation process when images were uploaded. The solution was to create a separate WebP thumbnail automatically with the GD library. Each image is resized to 800 pixels wide and saved at quality 80. Existing images were handled with a batch script so that the entire archive could be brought into the new format at once.

The thumbnails use a separate directory while preserving the same album and filename structure:

   uploads/album/相册名/photo.jpg ← 原图
   uploads/thumb/相册名/photo.webp ← 缩略图

The album scanner and image grid now use the WebP files for their src values. Full-size originals remain available when an image is opened. To avoid broken images if a thumbnail is missing, returns a 404, or the server cannot serve WebP, the selected fallback is:

   <img src="缩略图.webp" onerror="this.onerror=null;this.src='原图.jpg';">

This means the page normally loads small files of about 20–80 KB instead of immediately requesting originals that may be around 10 MB. The thumbnails make the album open much faster, while viewing a full-size image still loads the original as before. If a thumbnail disappears or the site is moved to a server without WebP support, the browser automatically falls back to the original rather than leaving a broken image.

The first run of the batch script produced empty directories. The problem turned out to be GLOB_BRACE failing to match image files because of uppercase and lowercase extension differences. A diagnostic script helped locate the issue, after which the matching logic was corrected. The album currently contains six directories: alone, beauty, dream, lifedrop, static, and todsay.

3. A collapsible archive by year

The archive page used to display every year in full, making the list unnecessarily long and making older posts difficult to find. There was already a partial implementation in the code: $open = $number === 0 ? null : " closed" had been prepared, but the required CSS and click behavior were missing, so the class had no visible effect.

The completed version adds the missing pieces:

  • .archive-year is made clickable and receives a ::before arrow. The arrow points down for an open section and right for a folded one.
  • .archive-list.closed { display:none } controls whether a year’s list is visible.
  • PHP marks the current year—the first year in the archive—as open by default. Older years receive the folded class. An incorrectly placed flex flex-col class inside an id attribute was also corrected.
  • JavaScript uses nextElementSibling to find the <ol> immediately following the year heading, then toggles that list when the heading is clicked.
  • The year heading also shows the actual number of posts after status-type entries have been excluded from the count.

The current year therefore opens with a downward ▼ arrow, while previous years start collapsed with a right-facing ▶ arrow. Clicking a year heading expands or hides its list, and the displayed count reflects real articles rather than including short status posts.

What changed overall

The friends page now keeps the full feed history while loading it in manageable pages, without making other requests wait. The album uses lightweight WebP thumbnails but still preserves the original images for full-size viewing, with an automatic fallback when thumbnails are unavailable. The archive presents the newest year first and keeps older years folded until they are needed.

These were not large visual redesigns, but they removed several small sources of friction: an artificially short feed list, oversized image requests, blocked page transitions, and an archive that was difficult to scan.