Visitar URL original
Layouts and transforms run (and stay cached) for pages that are never written · Issue #4399 · 11ty/buildawesome · GitHub
Skip to content

Layouts and transforms run (and stay cached) for pages that are never written #4399

Description

@j9t

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:

  1. The full layout chain and all transforms still run for every such page. A transform that logs this.page.outputPath gets called with false for each one.
  2. The result stays referenced through template._cacheRenderedTransformsAndLayoutsPromise until 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

  1. added this to the Core v4.0.0 milestone on Oct 16, 2026
  2. zachleat commented on Nov 3, 2026

    @zachleat
    Member

    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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions