Skip to content

Conversation

@p-jackson
Copy link
Member

Fixes DOTMSD-913

Proposed Changes

  • Add preload=viewport to the site list link. This will cause the Site object to get primed in the query cache
  • Ensure both site-by-id and site-by-slug caches are primed.

Why are these changes being made?

Before elastic search, this optimisation of pre-filling the site-by-slug cache was done when the /me/sites endpoint was loaded.

sites.forEach( ( site ) => {
const updater = ( oldData?: Site ) => ( oldData ? deepmerge( oldData, site ) : site );
queryClient.setQueryData( siteBySlugQuery( site.slug ).queryKey, updater );
queryClient.setQueryData( siteByIdQuery( site.ID ).queryKey, updater );
} );

This worked because we knew the /me/sites and /sites/%s response objects had the same format. But with ES we know the site list is now backed by site objects that don't match the Site data type. Site is still the definitive type for representing a site in the MSD, so we should make sure the site list loads it.

We only preload when the link is in "viewport" because the user could have a large page size. Preloading all the Site objects wouldn't be that bad if the user keeps the page size small.

CC @arthur791004 since I know you've been thinking about this optimisation.

Testing Instructions

  • /sites?flags=dashboard/v2/es-site-list
  • Make your viewport small
  • Delete the REACT_QUERY_OFFLINE_CACHE local storage
  • Refresh the page
  • Note which site objects are fetched in the network tools
  • Scroll down the page
  • Note which site objects are fetched in the network tools
  • Navigate to a site overview page, and there shouldn't be a long load time

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

@p-jackson p-jackson requested a review from a team as a code owner December 3, 2025 08:34
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Dec 3, 2025
@p-jackson p-jackson self-assigned this Dec 3, 2025
@matticbot
Copy link
Contributor

matticbot commented Dec 3, 2025

This PR modifies the release build for the following Calypso Apps:

For info about this notification, see here: PCYsg-OT6-p2

  • blaze-dashboard
  • notifications
  • wpcom-block-editor

To test WordPress.com changes, run install-plugin.sh $pluginSlug DOTMSD-913-pre-fetch-site-object on your sandbox.

Comment on lines 23 to 32
const site = await fetchSite( siteSlug );
queryClient.setQueryData( [ 'site-by-id', site.ID, SITE_FIELDS, SITE_OPTIONS ], site );
return site;
Copy link
Member Author

Choose a reason for hiding this comment

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

This feels kinda hacky, but queries don't have onSuccess callbacks (by design).

Another option would be to do this just in the overview page preloading code

Then it would only work in the specific overview page case. But would maybe be less hacky?

Copy link
Contributor

@arthur791004 arthur791004 Dec 3, 2025

Choose a reason for hiding this comment

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

How if we use the initialData to get the cache from another query?

initialData: () => {
  const cachedSites = queryClient.getQueriesData(['site-by-id']);
  return cachedSites
    .map(([key, data]) => data)
    .find(site => site.slug === siteSlug);
},

Copy link
Member Author

Choose a reason for hiding this comment

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

I read through the initialData docs and this looks like the perfect use case!

I've also made sure to set the initialDataUpdatedAt field. Otherwise, the siteByIdQuery might immediately fetch the site by ID anyway, even when it's not needed. Setting initialDataUpdatedAt means that TanStack will know how long ago the data was fetched and whether it should update it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I still don't get it. Why do we need / why are we able to set the initial data of siteBySlugQuery() from siteByIdQuery()?. When we open the Site List, we don't populate siteByIdQuery() at all. So I'm not sure how this initialData logic is helpful here 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

When we open the Site List, we don't populate siteByIdQuery() at all. So I'm not sure how this initialData logic is helpful here 🤔

We populate it by preload="viewport". With this setting, we preload the link when it's in the viewport so the siteByIdQuery will be pre-warmed

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm but it will preload the siteBySlugQuery(), not siteByIdQuery(), no? Or could you point me to the code/logic that does that.

I tested by removing the initialData and initialDataUpdatedAt and everything seems to work 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is mainly to ensure we can share the cache if one of them has already preloaded it. If any part of the app needs siteByIdQuery, we won’t need to request it again as long as the cached data hasn’t expired.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ohhh sorry I get it now. This logic is present in both of the queries 🤦‍♂️, I missed it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the way to think about it is that we are replacing the old optimisation, but we are doing it in quite a different way. The main thing is to ensure the navigation from the site list to the site-specific views is seamless.

Previously it was more about proactively pre-filling the site-by-slug and site-by-id query caches. Now the site-by-slug cache just happens to get pre-filled, but it is more declarative, we're just declaring that we would like to preload the overview link. And as you point out, site-by-id isn't really being pre-fetched at all. We're more lazily sharing the cached data. But the effect is the same—we're making sure the Site object is available to use immediately if we happen to have it locally.

@matticbot
Copy link
Contributor

matticbot commented Dec 3, 2025

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

App Entrypoints (~236 bytes added 📈 [gzipped])

name                    parsed_size           gzip_size
entry-dashboard-dotcom       +482 B  (+0.0%)     +123 B  (+0.0%)
entry-dashboard-ciab         +482 B  (+0.0%)     +108 B  (+0.0%)

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.

Sections (~350 bytes added 📈 [gzipped])

name                        parsed_size           gzip_size
staging-site                     +501 B  (+0.0%)     +124 B  (+0.0%)
sites-dashboard                  +501 B  (+0.0%)     +124 B  (+0.0%)
site-settings                    +501 B  (+0.0%)     +124 B  (+0.0%)
site-performance                 +501 B  (+0.0%)     +124 B  (+0.0%)
site-monitoring                  +501 B  (+0.0%)     +124 B  (+0.0%)
site-logs                        +501 B  (+0.0%)     +124 B  (+0.0%)
plans                            +501 B  (+0.0%)     +124 B  (+0.0%)
overview                         +501 B  (+0.0%)     +124 B  (+0.0%)
hosting                          +501 B  (+0.0%)     +124 B  (+0.0%)
github-deployments               +501 B  (+0.0%)     +124 B  (+0.0%)
domains                          +501 B  (+0.0%)     +124 B  (+0.0%)
marketplace                      +482 B  (+0.1%)     +105 B  (+0.0%)
activity                         +482 B  (+0.1%)     +121 B  (+0.1%)
a8c-for-agencies-sites           +482 B  (+0.0%)     +110 B  (+0.0%)
a8c-for-agencies-reports         +482 B  (+0.0%)     +110 B  (+0.0%)
a8c-for-agencies-referrals       +482 B  (+0.0%)     +110 B  (+0.0%)

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.

Legend

What is parsed and gzip size?

Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

@p-jackson p-jackson force-pushed the DOTMSD-913-pre-fetch-site-object branch from e8ff97b to afc98ad Compare December 5, 2025 02:57
render( <SiteLogs logType={ initialLogType } /> );

// Wait for data and TabPanel to render
await waitFor( () => expect( nock.isDone() ).toBe( true ) );
Copy link
Member Author

Choose a reason for hiding this comment

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

Removing these network checks to get the tests passing. The new initialData means the network call is no longer needed.

But we shouldn't need to wait explicitly for network calls to finish. We're already using findBy* queries, which have a wait built in: https://testing-library.com/docs/dom-testing-library/api-async#findby-queries

Waiting for UI elements that the user would see is better.

initialDataUpdatedAt: () => {
const site = getFromCache();
if ( site?.ID ) {
return queryClient.getQueryState( [ 'site-by-id', site.ID, SITE_FIELDS, SITE_OPTIONS ] )
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this?

Suggested change
return queryClient.getQueryState( [ 'site-by-id', site.ID, SITE_FIELDS, SITE_OPTIONS ] )
return queryClient.getQueryState( siteByIdQuery( site.ID ).queryKey )

// Used to find an existing Site object which is already in the `site-by-id` cache.
const getFromCache = () =>
queryClient
.getQueriesData< Site >( { queryKey: [ 'site-by-id' ] } )
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this must be exactly this otherwise it might get stale object whenever we update SITE_FIELDS or SITE_OPTIONS (not sure)

Suggested change
.getQueriesData< Site >( { queryKey: [ 'site-by-id' ] } )
.getQueriesData< Site >( siteByIdQuery( site.ID ) )

// Used to find an existing Site object which is already in the `site-by-slug` cache.
const getFromCache = () =>
queryClient
.getQueriesData< Site >( { queryKey: [ 'site-by-slug' ] } )
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comment here.

initialDataUpdatedAt: () => {
const site = getFromCache();
if ( site?.ID ) {
return queryClient.getQueryState( [ 'site-by-slug', site.slug, SITE_FIELDS, SITE_OPTIONS ] )
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comment here.

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

Labels

[Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants