Skip to content

Improved boot time by dropping the date-fns barrel import - #29771

Merged
vershwal merged 3 commits into
mainfrom
perf-drop-date-fns-barrel-from-boot-path
Aug 5, 2026
Merged

Improved boot time by dropping the date-fns barrel import#29771
vershwal merged 3 commits into
mainfrom
perf-drop-date-fns-barrel-from-boot-path

Conversation

@vershwal

@vershwal vershwal commented Aug 5, 2026

Copy link
Copy Markdown
Member

A bisect of Ghost's boot time found a step from ~1300ms to ~1420ms at 93faa3e ("Switched route settings to read through the configured store", #29305). This removes the cause: an accidental date-fns barrel import on the boot path.

What happened

#29305 changed routeSettings.init() to resolve its store through adapterManager.getAdapter('route-settings'). That loads FileStore.tsutils.ts, and utils.ts did:

import {format} from 'date-fns';

That's the barrel — 313 modules. Before the commit, the boot path only loaded the subpath require('date-fns/format') (35 modules) via settings-path-manager.js, which has since been deleted. So this was a straight regression rather than a newly introduced cost, and it has been on main since 15 July. adapterManager.init() now resolves every configured adapter in initCore, so it currently lands even earlier in boot than it did then.

The import exists to build a timestamped backup filename in getBackupRouteSettingsFilePath(). Its only callers are FileStore.replace() and S3RouteSettingsStore.replace() — the routes.yaml upload path. Boot never calls it. We were loading ~300 extra modules on every boot for a code path that only runs when someone uploads a routes.yaml.

Evidence

Controlled A/B at 93faa3e — require cache pre-seeded to match what is already resident at that point in boot, then load exactly what the commit newly pulls in:

time new modules
as shipped 121.4 ms 305
identical, barrel stubbed out 13.3 ms 18

The barrel accounts for ~108ms of the 121ms (89%) and 287 of the 305 net-new modules.

This is independently corroborated by a separate benchmark on the release build, which measured +264 files in require.cache versus 6.52.x (3,853 → 4,117). The predicted net-new from the barrel is +272 date-fns modules (+287 including the @babel/runtime helpers it pulls), against a baseline that already had 41 resident — date-fns/format from settings-path-manager.js plus date-fns/add/sub from @tryghost/nql-lang. Two different metrics on two machines converging rules out a local artifact.

Why hand-rolled rather than the subpath import

import format from 'date-fns/format' would recover ~115ms of the ~127ms available, so on milliseconds alone it is nearly as good. The reason to go further:

  • It makes the regression un-reintroducible. With date-fns gone from ghost/core/package.json, pnpm's strict node_modules means require('date-fns') from ghost/core now fails with MODULE_NOT_FOUND (verified). The subpath form leaves the barrel one "organise imports" or one codemod away from silently re-landing all 120ms, with nothing in the test suite to catch it. For a regression that sat on main undetected for three weeks, durability matters more than the marginal 12ms.
  • The subpath ages badly. date-fns v4 (already in the tree transitively) exports format as a named export only — format.d.ts has no default export — so import format from 'date-fns/format' becomes a type error on any upgrade, and the natural fix a future author reaches for is the barrel form.
  • utils.ts was the last date-fns reference anywhere in ghost/core, so removing the import lets the direct dependency go too. Note this is not an install-size win — date-fns stays in the lockfile via @tryghost/nql-lang and ember-template-lint. It is purely a boot-path win.

There is no library behaviour being reimplemented here: yyyy-MM-dd-HH-mm-ss is six zero-padded local-time numeric fields with no locale- or timezone-dependent tokens.

Verification

  • Output is identical to the previous implementation for any valid Date, verified across a 200k random-date fuzz over 1970–2039 with 0 mismatches, run under several timezones including America/New_York and half-hour-DST zones, covering both DST transitions, leap days, year boundaries and all-single-digit fields. (It intentionally differs on inputs the sole caller cannot produce — an invalid Date yields NaN fields where date-fns throws, and there is no era handling below 1 AD. Both are documented in the JSDoc.)
  • Added a test with a fixed system time pinning the zero-padding. CI runs the unit suite in America/New_York, so it also fails if the implementation switches to getUTC* accessors.
  • pnpm exec vitest run test/unit/server/adapters/route-settings/ — 46 tests across 4 files pass, including under TZ=America/New_York.
  • eslint clean on both changed files; tsc --noEmit shows no new errors (the one reported error in members-custom-fields/values-service.ts is pre-existing on main).
  • Confirmed require('./core/server/adapters/route-settings/FileStore') now loads 0 date-fns modules, down from 313.
  • Lockfile diff is 3 lines — only the ghost/core importer entry.

Follow-ups (deliberately not in this PR)

  • A boot-time guard in CI. We had no boot signal at all, which is why this went unnoticed for three weeks. A require.cache size assertion after boot would have caught it at review time and, unlike a wall-clock threshold, wouldn't be flaky. Worth pairing with a coarse timing check, since module count is a proxy for cost rather than the cost itself.
  • getBackupRedirectsFilePath() in custom-redirects/utils.ts is a near-identical twin of this helper but formats via moment-timezone, which overrides.js pins to UTC — so the two mirrored adapter families currently write differently-zoned backup filenames. Pre-existing, not touched here, but worth a ticket.

@nx-cloud

nx-cloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 506fb8b

Command Status Duration Result
nx run ghost:test:ci:integration ✅ Succeeded 1m 53s View ↗
nx run ghost:test:integration ✅ Succeeded 3m 20s View ↗
nx run @tryghost/admin:test:acceptance ✅ Succeeded 6m 5s View ↗
nx run ghost:test:legacy ✅ Succeeded 3m 1s View ↗
nx run @tryghost/koenig-lexical:test:acceptance ✅ Succeeded 2m 27s View ↗
nx run ghost:test:e2e ✅ Succeeded 2m 41s View ↗
nx run ghost-monorepo:lint:boundaries ✅ Succeeded 24s View ↗
nx run-many -t test:unit -p ghost,@tryghost/ada... ✅ Succeeded 39s View ↗
Additional runs (8) ✅ Succeeded ... View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-08-05 10:46:48 UTC

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 29a16d6c-c076-4662-8312-c4c4d1ed0016

📥 Commits

Reviewing files that changed from the base of the PR and between 97fcc57 and 76efe42.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • ghost/core/core/server/adapters/route-settings/utils.ts
  • ghost/core/package.json
  • ghost/core/test/unit/server/adapters/route-settings/utils.test.ts
💤 Files with no reviewable changes (1)
  • ghost/core/package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • ghost/core/core/server/adapters/route-settings/utils.ts
  • ghost/core/test/unit/server/adapters/route-settings/utils.test.ts

Walkthrough

The route-settings backup path now uses local pad and timestamp helpers instead of date-fns. The timestamp format remains yyyy-MM-dd-HH-mm-ss. The production date-fns dependency was removed. Unit tests restore real timers and verify zero-padding for date and time values.

Possibly related PRs

Suggested reviewers: allouis

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the boot-time improvement and removal of the date-fns barrel import, which are the main changes.
Description check ✅ Passed The description directly explains the boot-time regression, the date-fns removal, the replacement implementation, and verification results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf-drop-date-fns-barrel-from-boot-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.59%. Comparing base (7bfff19) to head (76efe42).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #29771      +/-   ##
==========================================
- Coverage   75.59%   75.59%   -0.01%     
==========================================
  Files        1613     1613              
  Lines      142650   142620      -30     
  Branches    17631    17647      +16     
==========================================
- Hits       107842   107810      -32     
+ Misses      33759    33732      -27     
- Partials     1049     1078      +29     
Flag Coverage Δ
e2e-tests 77.72% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

ref #29305

- a bisect of boot time found a ~120ms step at #29305, which moved the
  route-settings read path onto the adapter-backed store; the store's module
  graph reaches utils.ts, which imported the whole date-fns barrel (313 modules)
- before that commit the boot path only loaded the date-fns/format subpath (35
  modules) via the since-deleted settings-path-manager, so this was a straight
  regression rather than a newly introduced cost
- format is only used to build the timestamped backup filename in
  getBackupRouteSettingsFilePath, whose only callers are FileStore.replace and
  S3RouteSettingsStore.replace - the routes.yaml upload path. Boot never runs it
- hand-rolled the timestamp rather than switching to the subpath import: the
  subpath still loads 35 modules at boot, and leaves the barrel one "organise
  imports" away from silently re-landing. Removing the dependency instead makes
  that un-reintroducible - date-fns no longer resolves from ghost/core at all
- yyyy-MM-dd-HH-mm-ss has no locale- or timezone-dependent tokens, so the output
  is unchanged for any valid Date
@vershwal
vershwal force-pushed the perf-drop-date-fns-barrel-from-boot-path branch from 506fb8b to 8b401e8 Compare August 5, 2026 10:35
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Removed comments explaining the timestamp function and its limitations.
Removed comments explaining timestamp formatting expectations.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes an accidental date-fns barrel import from Ghost Core’s boot path by replacing it with a small, local timestamp formatter used only for route-settings backup filenames. This improves startup performance by avoiding eagerly loading hundreds of date-fns modules during initialization.

Changes:

  • Replaced date-fns format() usage with a lightweight local timestamp() formatter in the route-settings adapter utility.
  • Added a unit test that pins zero-padding behavior using Vitest fake timers.
  • Removed date-fns as a direct dependency of ghost/core (and updated the lockfile importer entry accordingly).

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated no comments.

File Description
ghost/core/core/server/adapters/route-settings/utils.ts Drops date-fns import and implements a local timestamp formatter for backup filenames.
ghost/core/test/unit/server/adapters/route-settings/utils.test.ts Adds a deterministic test to assert correct zero-padding via fixed system time.
ghost/core/package.json Removes date-fns from direct dependencies in ghost/core.
pnpm-lock.yaml Removes the ghost/core importer’s date-fns entry to match dependency removal.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vershwal
vershwal merged commit 784f22c into main Aug 5, 2026
55 checks passed
@vershwal
vershwal deleted the perf-drop-date-fns-barrel-from-boot-path branch August 5, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants