Skip to content

Faster builds: URLs without Antlers for simple routes, and a lazy dirty-state snapshot - #3

Merged
DanielDarrenJones merged 6 commits into
mainfrom
faster-build
Nov 10, 2026
Merged

DanielDarrenJones merged 6 commits into
mainfrom
faster-build

Conversation

@DanielDarrenJones

Copy link
Copy Markdown
Contributor

Profiling flatpack:build on the demo's largest set (9,000 entries, nested page builder) showed that YAML parsing is now about 1% of the build. The time goes on building entry URLs and on reading entries back from the store. This PR takes two pieces of that.

Results

Demo, nested page builder, 9,000 entries. Released 1.1.0 and this branch built alternately, median of 5 (Apple M1 Pro, PHP 8.5, Statamic 6.40; the machine was busy, so read the difference rather than the absolute times):

1.1.0 This PR
flatpack:build 21.5 s 15.5 s (−28%)
Peak memory 356 MB 285 MB (−20%)
Reading 9,000 entries through a query 81 µs each 32 µs each

URLs for simple routes

Statamic builds each entry's URL by converting its route to Antlers and parsing it, and Antlers::parse() makes a new parser from the container every time (with about 14 config reads). On this build that was 19,560 parsers.

FastUrlBuilder (bound for Statamic\Contracts\Routing\UrlBuilder) handles routes that are only placeholders, such as /books/{slug} or {parent_uri}/{slug}. It puts the values in and hands the result to Statamic's own build() for the rest (slugifying and tidying, which differ between Statamic 5 and 6), using Antlers::usingParser() with a parser that returns its text unchanged.

Statamic's builder is used untouched when:

  • the route is written in Antlers ({{ ... }});
  • a placeholder name isn't a plain identifier, or is a word Antlers gives its own meaning;
  • a placeholder has no value, or its value isn't a string or integer (lists, Value objects);
  • a value, or what's left of the route, has braces in it.

Dirty-state snapshot only when asked

Statamic calls syncOriginal() on every entry read from the Stache, which builds an array of the entry's slug, date, path, origin and all its data. During a build each entry is read about seven times.

FlatpackEntry::syncOriginal() now keeps a clone of the entry as it was read (with its own copy of the data, supplements and date) and works the snapshot out from that clone when getOriginal(), isDirty() or __sleep() is called. This applies to normal requests too, not just builds.

Checks

  • Every entry and term URL on the demo (12,840) is identical to 1.1.0's.
  • bin/parity passes locally on Statamic 6 (latest) and Statamic 5 (latest). 5.11.0 wouldn't install on this machine, so that one is left to CI.
  • The parity fixtures gain two collections that can't take the fast path: one with an Antlers route, and one whose placeholders are a list, a missing value and a value with braces.

Not in this PR

  • Keeping decoded entries in memory during the build: the profile suggests about 2%, with a risk of handing out a stale entry.
  • The rest is Statamic's own work during index building (terms created through the container, Str::slug reading config on each call), which would be changes to Statamic itself.

…y state lazily

Statamic builds every entry's URL by running its route through Antlers with
a new parser each time. For routes that are only placeholders, FastUrlBuilder
puts the values in itself and lets Statamic's builder do the rest (slugify,
tidy) with a parser that has nothing to do. Anything else is left to Statamic.

FlatpackEntry no longer works out its dirty-state snapshot on every read. It
keeps a copy of the entry as read and derives the snapshot when getOriginal(),
isDirty() or serialisation asks.

The parity fixtures gain two collections whose routes can't take the fast path.
# Conflicts:
#	CHANGELOG.md
#	src/ServiceProvider.php
A placeholder named merge, pluck, where, take, skip, orderby, groupby or arr
is read by Antlers as part of its language even when the entry has a value
of that name, so those go to Statamic's builder too.
…he parity suite, fix the 5.11 install

The lazy dirty-state snapshot is taken from a copy of the entry, but Statamic
remembers origins by entry id, so the copy saw the entry's current origin.
The copy is now shown the origin it was read with while the snapshot is
worked out. Older Statamic versions don't give a clone its own data, so the
copy gets one.

The parity suite compares what entries report as their original values and
dirty state after being changed in memory (origin changed or removed, data,
slug, date moved in place, serialised, cloned).

bin/parity now lets Composer update every package when installing a Statamic
version: the skeleton pins laravel/pail to a release that needs Laravel 12,
which Statamic 5.11 can't use.
@DanielDarrenJones
DanielDarrenJones merged commit a75017e into main Nov 10, 2026
4 checks passed
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.

1 participant