Skip to content

perf: lazy-load AI Content Planner from the block editor#23214

Open
enricobattocchi wants to merge 1 commit intorelease/27.6from
lazy-content-planner
Open

perf: lazy-load AI Content Planner from the block editor#23214
enricobattocchi wants to merge 1 commit intorelease/27.6from
lazy-content-planner

Conversation

@enricobattocchi
Copy link
Copy Markdown
Member

Context

  • The 27.6 release zip crossed the 5 MB mark (5,235,914 → 5,370,411 bytes vs 27.5). Investigation traced ~95 KB compressed of that growth to the AI Content Planner module being bundled twice: statically imported into block-editor.js (+161 KB raw / +28 KB gzipped) and shipped as a separate standalone ai-content-planner.js entry (~177 KB raw / 54 KB gzipped).
  • The standalone bundle's initialize.js only exports initContentPlanner() and never invokes it — so on Elementor and classic-editor pages, where the standalone bundle was the only enqueue path, the planner UI never actually mounted. The bundle was effectively dead weight. The Block Editor flow worked because block-editor-integration.js separately calls initContentPlanner().
  • This PR splits the planner into a lazy-loaded chunk under a single webpackChunkName, removes the redundant standalone entry, and rewires the PHP integration to localize wpseoContentPlanner onto the block-editor handle.

Summary

This PR can be summarized in the following changelog entry:

  • Improves block editor load time by lazy-loading the AI Content Planner module on demand.

Relevant technical choices:

  • block-editor-integration.js now uses dynamic import() to load ai-content-planner/initialize only when getIsAiFeatureEnabled() is true; both fills (SidebarFill.js, MetaboxFill.js) use React.lazy + Suspense for ContentPlannerEditorItem. All three call sites share the same webpackChunkName: "ai-content-planner-editor" so webpack emits a single async chunk.
  • Dynamic import(), React.lazy and Suspense are first-uses in this codebase. Webpack 5's output.publicPath: "auto" resolves the chunk URL from document.currentScript.src at runtime, which matches our /wp-content/plugins/wordpress-seo/js/dist/ layout.
  • The standalone ai-content-planner webpack entry is dropped from config/webpack/paths.js. The script handle disappears from the regenerated src/generated/assets/plugin.php automatically (the asset manager auto-registers from that manifest).
  • Content_Planner_Integration::enqueue_assets now localizes wpseoContentPlanner onto the block-editor handle (which lazy-loads the planner). The elementor/editor/before_enqueue_scripts hook is removed: it was only enqueuing the dead-weight bundle. If/when the planner is wanted in Elementor, the right pattern is the AI generator one — a domReady(() => init…()) self-bootstrap inside initialize.js plus an Elementor mount path inside the React tree.
  • Net js/dist/ size delta vs 27.6 (gzip-9, summed across all files): -25.8 KiB. block-editor.js raw: 368 KB → 264 KB (-104 KB raw / -28 KB gzipped). The new async chunk is ~91 KB raw / 23 KB gzipped, plus a small ~6 KB shared helper.
  • No new unit tests added: the PHP change is a 3-line refactor with no new logic, and the JS changes are pure wiring (replacing static imports with dynamic equivalents, no new branches to cover). All 4973 PHPUnit tests and 1380 Jest tests pass locally.

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

  1. Check out this branch and run a production build (yarn build or grunt build).
  2. Verify js/dist/ai-content-planner.js no longer exists, but js/dist/ai-content-planner-editor.js does (along with a small numbered chunk like 833.js).
  3. In a WordPress install with Yoast SEO active and the AI features enabled, open a Post in the Block Editor.
  4. Open the browser DevTools Network tab and reload. Confirm block-editor.js loads, then ai-content-planner-editor.js is fetched on demand shortly after (deferred chunk).
  5. Open the Yoast sidebar — the Content Planner item should appear and behave exactly as on 27.6: open the modal, generate suggestions, generate an outline, and verify the Content Planner Banner block is auto-inserted on a new post.
  6. Disable the AI feature (Yoast → Settings) and reload a post. Confirm block-editor.js loads but ai-content-planner-editor.js is not fetched.
  7. Open the same post in Elementor. Confirm the planner UI is absent (same as on 27.6) and that no ai-content-planner*.js file is requested.
  8. Open the same post in the Classic Editor. Same expectation as Elementor.

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite
  • Console: watch for chunk-loading errors (ChunkLoadError, 404 on ai-content-planner-editor.js, etc.) — this is the first use of webpack code-splitting in the plugin.
  • Editors: the most important coverage is the Block Editor (planner runs there). Elementor and Classic editor are covered to confirm the previously-shipped-but-non-functional bundle is no longer loaded — a regression here would be the planner UI failing to appear in Block Editor, or appearing where it didn't before.
  • Browsers: dynamic import() is supported across all browsers we already target, but a quick sanity check on Chrome / Firefox / Safari is worthwhile because this is a new pattern in our build.

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

Impact check

This PR affects the following parts of the plugin, which may require extra testing:

  • The Block Editor integration (packages/js/src/initializers/block-editor-integration.js and its sidebar/metabox fills) — the AI Content Planner UI is now loaded asynchronously instead of synchronously.
  • WPSEO_Admin_Asset_Manager's registered script handles — the ai-content-planner handle is removed (auto, via the regenerated src/generated/assets/plugin.php manifest).
  • Content_Planner_Integration::enqueue_assets — now localizes onto block-editor instead of ai-content-planner. Behaviour in the Block Editor is unchanged; behaviour in Elementor / classic-editor changes from "ship 177 KB of dead-weight JS" to "ship nothing", which is what was already happening at the user-experience level (no UI ever mounted there).

Other environments

  • This PR also affects Shopify. I have added a changelog entry starting with [shopify-seo], added test instructions for Shopify and attached the Shopify label to this PR.
  • This PR also affects Yoast SEO for Google Docs. I have added a changelog entry starting with [yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached the Google Docs Add-on label to this PR.

Documentation

  • I have written documentation for this change. For example, comments in the Relevant technical choices, comments in the code, documentation on Confluence / shared Google Drive / Yoast developer portal, or other.

Quality assurance

  • I have tested this code to the best of my abilities.
  • During testing, I had activated all plugins that Yoast SEO provides integrations for.
  • I have added unit tests to verify the code works as intended.
  • If any part of the code is behind a feature flag, my test instructions also cover cases where the feature flag is switched off.
  • I have written this PR in accordance with my team's definition of done.
  • I have checked that the base branch is correctly set.
  • I have run grunt build:images and committed the results, if my PR introduces or edits images or SVGs.

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label.
  • I have added my hours to the WBSO document.

Fixes #


Local checks status (run before opening this PR):

  • composer check-branch-cs ✅ (1 file checked, no errors)
  • composer lint-branch ✅ (1 file checked, no syntax errors)
  • composer test ✅ (4973 tests, 26234 assertions, all pass)
  • yarn lint ✅ in packages/js (3 errors / 43 warnings — same as trunk baseline; no new errors or warnings introduced)
  • yarn test ✅ in packages/js (1380 tests across 155 suites, all pass)
  • composer test-wp-env ⚠️ not run (requires Docker wp-env, not available in this environment) — disclosed for QA awareness
  • Manual smoke test in a real WordPress install ⚠️ not run — the lazy-load path uses import() / React.lazy for the first time in this codebase. CI plus QA acceptance testing should validate chunk-loading in Block Editor / Elementor / Classic editor before merge.

The 27.6 release pushed the plugin zip past the 5 MB mark. The dominant
cause was the AI Content Planner module being bundled twice:

* statically imported from packages/js/src/initializers/block-editor-integration.js
  and the Sidebar/Metabox fills, so it landed in block-editor.js
  (+161 KB raw / +28 KB gzipped);
* shipped a second time as the standalone js/dist/ai-content-planner.js
  entry (~177 KB raw / 54 KB gzipped). That bundle's initialize.js only
  exports initContentPlanner() and never self-bootstraps, so on Elementor
  and classic-editor pages — where the standalone bundle was the only one
  enqueued — the planner UI never actually mounted. The bundle was dead
  weight there.

Convert the three import sites to dynamic import() / React.lazy + Suspense
under a single webpackChunkName, then drop the standalone webpack entry
and localize wpseoContentPlanner onto block-editor instead. The Elementor
enqueue hook is removed too: it was only ever shipping the dead-weight
bundle.

Net js/dist gzipped delta vs 27.6: -25.8 KiB. The planner code now ships
once, on demand, and block-editor.js is ~28 KiB gzipped lighter for users
without the AI feature enabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@enricobattocchi enricobattocchi added the changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog label Apr 29, 2026
@enricobattocchi enricobattocchi changed the base branch from trunk to release/27.6 April 29, 2026 11:57
@enricobattocchi enricobattocchi added changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog and removed changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog labels Apr 29, 2026
@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 29

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage decreased (-1.7%) to 51.724%

Details

  • Coverage decreased (-1.7%) from the base build.
  • Patch coverage: 6 uncovered changes across 4 files (0 of 6 lines covered, 0.0%).
  • 3 coverage regressions across 2 files.

Uncovered Changes

File Changed Covered %
packages/js/src/initializers/block-editor-integration.js 2 0 0.0%
src/ai/content-planner/user-interface/content-planner-integration.php 2 0 0.0%
packages/js/src/components/fills/MetaboxFill.js 1 0 0.0%
packages/js/src/components/fills/SidebarFill.js 1 0 0.0%

Coverage Regressions

3 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
src/ai/content-planner/user-interface/content-planner-integration.php 2 0.0%
packages/js/src/components/fills/MetaboxFill.js 1 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 12379
Covered Lines: 6232
Line Coverage: 50.34%
Relevant Branches: 3488
Covered Branches: 1975
Branch Coverage: 56.62%
Branches in Coverage %: Yes
Coverage Strength: 243002.18 hits per line

💛 - Coveralls

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

Labels

changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants