Repository navigation
Description
Successfully confirmed, patched, and verified in a 3.1.6 release.
AI-assisted issue report (Claude Opus 5.5):
Affects 3.1.6 and 4.0.0-alpha.10 (the latest canary). The code below is the same in both.
Problem
Template.generateMapEntry() renders every renderable page with its layouts and transforms before it checks whether the page is writeable:
// src/Template.js, generateMapEntry()
if (page.template.isRenderable()) {
// this reuses page.templateContent, it doesn’t render it
content = await page.template.renderPageEntry(page);
}
// …
if (!page.template.behavior.isWriteable()) {
debug("Template not written …");
continue;
}For pages with permalink: false (or permalink.build: false), the result is discarded, but:
- The full layout chain and all transforms still run for every such page. A transform that logs
this.page.outputPathgets called withfalsefor each one. - The result stays referenced through
template._cacheRenderedTransformsAndLayoutsPromiseuntil the build ends.
Sites that use permalink: false heavily (for example, link collections where most entries only feed collections, tag pages, and feeds) pay for this in memory and CPU. The rendered HTML is never used.
Impact (real-site numbers)
These numbers come from Frontend Dogma on Eleventy 3.1.6 and Node 26:
- 25,710 templates, of which ~24,400 have
permalink: false; 2,443 files written - Cached layout-rendered output for unwritten pages: ~106 million characters. Almost all of it is two-byte strings (because of characters like “·” or curly quotes in layouts), so ~210 MB of retained heap.
- Retained heap at the end of the build, after GC: 884 MB before, 672 MB after the change below
- Output is byte-identical (apart from timestamps and randomized content), and build time is unchanged or slightly better.
Proposed solution
Skip rendering layouts and transforms when the page won’t be written. Restrict this to fs output, so JSON output (and NDJSON in 3.x), which reports content for every page, keeps working as before:
// Note that behavior.render is overridden when using json or ndjson output
- if (page.template.isRenderable()) {
+ // Skip layouts and transforms for pages that won’t be written (e.g., `permalink: false`)
+ if (page.template.isRenderable() && (to !== "fs" || page.template.behavior.isWriteable())) {
// this reuses page.templateContent, it doesn’t render it
content = await page.template.renderPageEntry(page);
}templateContent is unaffected, because it is rendered separately (without layouts) when collections are populated. Collections, feeds, and anything else that reads templateContent keep working. Write results and eleventy.after results are unchanged as well, because unwritten pages were never part of them.
Behavior change to consider: transforms no longer run for pages that aren’t written. That seems correct (a transform operates on output), but a plugin that uses transforms for side effects on all pages would notice. If that matters, this could sit behind a config option, though skipping seems like the better default.
Related observation: global data is deep-copied into every template
While investigating, a much larger cost was identified (~1.15 GB on the same site). Merge (in @11ty/eleventy-utils, still the case in 2.0.7, which 4.0.0-alpha.10 uses) deep-copies plain objects from global data into each template’s data, unless the object is frozen. A 32 KB JSON file in _data/ (an object with 926 keys) therefore ended up as ~25,700 separate copies. Arrays and frozen objects are shared by reference.
A workaround: Moving such a file out of _data/ (done in this case). Still, the cost is easy to miss and grows with template count × global object size. Two possible fixes are sharing (or freezing) global data objects that templates don’t override, and documenting the behavior with a recommendation to keep large objects out of global data.
—Happy to file the second issue separately if preferred, and also to provide PRs if helpful.
Activity
zachleat commentedon Nov 3, 2026
Fix shipping with v4.0.0-alpha.11 thanks!
I didn’t fix the Merge issue you noted there, if you still want that one please file another issue!