Improved boot time by dropping the date-fns barrel import - #29771
Conversation
|
| 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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughThe route-settings backup path now uses local Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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
506fb8b to
8b401e8
Compare
|
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.
|
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. |
There was a problem hiding this comment.
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-fnsformat()usage with a lightweight localtimestamp()formatter in the route-settings adapter utility. - Added a unit test that pins zero-padding behavior using Vitest fake timers.
- Removed
date-fnsas a direct dependency ofghost/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.

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-fnsbarrel import on the boot path.What happened
#29305 changed
routeSettings.init()to resolve its store throughadapterManager.getAdapter('route-settings'). That loadsFileStore.ts→utils.ts, andutils.tsdid:That's the barrel — 313 modules. Before the commit, the boot path only loaded the subpath
require('date-fns/format')(35 modules) viasettings-path-manager.js, which has since been deleted. So this was a straight regression rather than a newly introduced cost, and it has been onmainsince 15 July.adapterManager.init()now resolves every configured adapter ininitCore, 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 areFileStore.replace()andS3RouteSettingsStore.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:
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.cacheversus 6.52.x (3,853 → 4,117). The predicted net-new from the barrel is +272 date-fns modules (+287 including the@babel/runtimehelpers it pulls), against a baseline that already had 41 resident —date-fns/formatfromsettings-path-manager.jsplusdate-fns/add/subfrom@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:date-fnsgone fromghost/core/package.json, pnpm's strictnode_modulesmeansrequire('date-fns')from ghost/core now fails withMODULE_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 onmainundetected for three weeks, durability matters more than the marginal 12ms.formatas a named export only —format.d.tshas no default export — soimport 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.tswas the lastdate-fnsreference 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-langandember-template-lint. It is purely a boot-path win.There is no library behaviour being reimplemented here:
yyyy-MM-dd-HH-mm-ssis six zero-padded local-time numeric fields with no locale- or timezone-dependent tokens.Verification
Date, verified across a 200k random-date fuzz over 1970–2039 with 0 mismatches, run under several timezones includingAmerica/New_Yorkand 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 invalidDateyieldsNaNfields where date-fns throws, and there is no era handling below 1 AD. Both are documented in the JSDoc.)America/New_York, so it also fails if the implementation switches togetUTC*accessors.pnpm exec vitest run test/unit/server/adapters/route-settings/— 46 tests across 4 files pass, including underTZ=America/New_York.eslintclean on both changed files;tsc --noEmitshows no new errors (the one reported error inmembers-custom-fields/values-service.tsis pre-existing onmain).require('./core/server/adapters/route-settings/FileStore')now loads 0 date-fns modules, down from 313.Follow-ups (deliberately not in this PR)
require.cachesize 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()incustom-redirects/utils.tsis a near-identical twin of this helper but formats viamoment-timezone, whichoverrides.jspins to UTC — so the two mirrored adapter families currently write differently-zoned backup filenames. Pre-existing, not touched here, but worth a ticket.