Skip to content

fix(newspack-popups): cache empty popups_for_post() results#482

Open
jhalitschke wants to merge 2 commits into
Automattic:mainfrom
jhalitschke:fix/popups-for-post-empty-result-cache
Open

fix(newspack-popups): cache empty popups_for_post() results#482
jhalitschke wants to merge 2 commits into
Automattic:mainfrom
jhalitschke:fix/popups-for-post-empty-result-cache

Conversation

@jhalitschke

Copy link
Copy Markdown

All Submissions:

Changes proposed in this Pull Request:

Re-opened against the monorepo — originally submitted as Automattic/newspack-popups#1563, auto-closed since that repo is now a read-only mirror.

Problem

Newspack_Popups_Inserter::popups_for_post() (plugins/newspack-popups/includes/class-newspack-popups-inserter.php) memoizes its result in self::$popups, guarded by:

protected static $popups = [];

public static function popups_for_post() {
	if ( ! empty( self::$popups ) ) {
		return self::$popups;
	}
	...
	self::$popups = $popups_to_display; // may be []
	return $popups_to_display;
}

empty( [] ) is always true, so whenever no campaign is eligible for the current request, the cache is written but never read back as a hit. Every subsequent call in the same request re-runs assess_has_disabled_popups(), parse_view_as(), Newspack_Popups_Model::retrieve_eligible_popups() (a WP_Query) and a should_display() pass over every campaign.

popups_for_post() is called from insert_popups_in_content(), insert_popups_after_header(), insert_before_header() and insert_inline_prompt_in_archive_pages(), so on an archive page this repeats once per post instead of once per request. On a site where no campaigns match the request context (e.g. all campaigns are scoped to a segment/view that excludes the current visitor), this turns into 40-50+ redundant query + evaluation passes on a single archive page load.

Fix

Use null instead of [] as the "not yet computed" sentinel, since an empty array is a legitimate, cacheable result:

protected static $popups = null;

public static function popups_for_post() {
	if ( null !== self::$popups ) {
		return self::$popups;
	}
	...
}

No other change in behavior: the IS_TEST_ENV branch already skips writing the cache, so tests are unaffected either way, and nothing else reads Newspack_Popups_Inserter::$popups directly (the data-api class has its own unrelated $popups property).

Closes # .

How to test the changes in this Pull Request:

  1. php -l on the changed file
  2. Verify prompts still render on posts/pages that have eligible campaigns
  3. Verify archive pages with zero eligible campaigns no longer re-run retrieve_eligible_popups() per post (checked via query count / profiling)

Other information:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes, as applicable?
  • Have you successfully ran tests with your changes locally?

self::$popups defaulted to [] and the cache guard was `! empty(
self::$popups )`, so a legitimate empty result (no popups eligible
for the current request) was indistinguishable from "not yet
computed". Every call re-ran assess_has_disabled_popups(),
parse_view_as(), retrieve_eligible_popups() and a should_display()
pass over every campaign — on archive pages this repeats once per
post instead of once per request.

Switch the memoization guard to null, since an empty array is a
valid, cacheable result.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 1, 2026 15:52
@jhalitschke jhalitschke requested a review from a team as a code owner July 1, 2026 15:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to reduce redundant work in newspack-popups by ensuring Newspack_Popups_Inserter::popups_for_post() memoization treats an empty result as a valid cache hit (using null as the “not computed yet” sentinel).

Changes:

  • Change Newspack_Popups_Inserter::$popups default from [] to null and update its docblock to reflect the new sentinel behavior.
  • Update the memoization guard in popups_for_post() from ! empty( self::$popups ) to null !== self::$popups so empty results are cached/hit.

Comment thread plugins/newspack-popups/includes/class-newspack-popups-inserter.php Outdated
Copilot flagged that assess_has_disabled_popups() and should_display()
both depend on the current post (post meta, post type, taxonomy terms),
so a single request-wide cache — which is what the previous fix in this
PR produced — can return a stale result for a post other than the one
it was computed for. On an archive page looping over posts with
different categories, this could suppress prompts that should display,
or show prompts that shouldn't, for every post after the first.

Split the cache into two tiers:
- self::$eligible_popups: the raw retrieve_eligible_popups() query
  result, which doesn't depend on the current post, memoized once per
  request as before (this is the expensive part — a WP_Query).
- self::$popups: the post-dependent should_display() filter result,
  now keyed by post ID instead of a single value, so different posts
  in the same request get their own cache entry.

array_key_exists() (rather than an empty()/null check) distinguishes
"not yet computed for this post ID" from "computed and empty", so the
original empty-result bug this PR set out to fix stays fixed on a
per-post basis.

Update the two tests that seed Newspack_Popups_Inserter::$popups
directly via reflection (test-block-theme-header-insertion.php,
test-overlay-queueing.php) to seed at the current post ID instead of
as a flat list, matching the new keyed shape. The existing
setValue(null, []) resets in those tests' set_up()/tear_down() are
unaffected, since an empty array still correctly means "nothing cached
for any post".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

2 participants