Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(presentation): use live content API for loaders #8429

Merged
merged 1 commit into from
Jan 30, 2025

Conversation

stipsan
Copy link
Member

@stipsan stipsan commented Jan 28, 2025

Description

This PR switches Presentation when used to power loader queries (this is what @sanity/react-loader's useLiveMode is using) from using a aggressive polling refetch approach that fires every two seconds, to Sanity Live Content with Drafts, which ensures refetches only happen when content has actually changed.

less.requests.mov

What to review

Hopefully there's enough context to understand the changes 🙌

Testing

New tests added for new state logic that are reducer based. There's still a lack of E2E tests so manual testing is required.

Notes for release

Presentation Tool is now using Sanity Live Content API to drive Live Mode in integrations like @sanity/react-loader, @sanity/svelte-loader, @nuxtjs/sanity and @sanity/core-loader.
The new setup allows query refetching to only happen when actually needed, as opposed to every two seconds.
Why have we been aggressively fetching every two seconds?
It's in order to ensure changes that can't reasonably be predicted (using Content Source Maps/CSM) client side are still caught, and eventual consistency in the live preview reached:

  • count(*[_type == "person" && isPublished])
    For the client to know when this query count will change it would need to monitor every person document and if isPublished is true. That doesn't scale.
  • *[slug.current == "discounted"]
    If any document is given the slug discounted, the query needs to refetch. The client would need to monitor every single document mutation in the dataset and check if a new document now has the slug, as well as check if any fields on the current matching document has changed, or if the matching document got deleted and there's a fallback match, or none at all.

The gist of it is the client can't predict everything, and if it tries, it winds up using a lot of memory and perform work, just in case anything has changed.
While this over-fetching never counted towards your Sanity API hit quota, the intention was to move over to a better architecture.
And that new API is Sanity Live; it's built to solve "when should a query refetch?" and it allows Presentation to rely on the backend for telling it when it needs to refetch, thus using far less memory, less network resources, and less work on the main thread.

Copy link

vercel bot commented Jan 28, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
page-building-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 30, 2025 3:30pm
performance-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 30, 2025 3:30pm
test-studio ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 30, 2025 3:30pm
2 Skipped Deployments
Name Status Preview Comments Updated (UTC)
studio-workshop ⬜️ Ignored (Inspect) Visit Preview Jan 30, 2025 3:30pm
test-next-studio ⬜️ Ignored (Inspect) Jan 30, 2025 3:30pm

Copy link
Contributor

No changes to documentation

Copy link
Contributor

github-actions bot commented Jan 28, 2025

Component Testing Report Updated Jan 30, 2025 3:34 PM (UTC)

❌ Failed Tests (3) -- expand for details
File Status Duration Passed Skipped Failed
comments/CommentInput.spec.tsx ✅ Passed (Inspect) 1m 6s 15 0 0
formBuilder/ArrayInput.spec.tsx ✅ Passed (Inspect) 12s 3 0 0
formBuilder/inputs/PortableText/Annotations.spec.tsx ❌ Failed (Inspect) 1m 59s 4 0 2
formBuilder/inputs/PortableText/copyPaste/CopyPaste.spec.tsx ✅ Passed (Inspect) 50s 11 7 0
formBuilder/inputs/PortableText/copyPaste/CopyPasteFields.spec.tsx ✅ Passed (Inspect) 0s 0 12 0
formBuilder/inputs/PortableText/Decorators.spec.tsx ✅ Passed (Inspect) 25s 6 0 0
formBuilder/inputs/PortableText/DisableFocusAndUnset.spec.tsx ✅ Passed (Inspect) 14s 3 0 0
formBuilder/inputs/PortableText/DragAndDrop.spec.tsx ✅ Passed (Inspect) 26s 6 0 0
formBuilder/inputs/PortableText/FocusTracking.spec.tsx ✅ Passed (Inspect) 1m 6s 15 0 0
formBuilder/inputs/PortableText/Input.spec.tsx ✅ Passed (Inspect) 1m 31s 21 0 0
formBuilder/inputs/PortableText/ObjectBlock.spec.tsx ✅ Passed (Inspect) 2m 3s 21 0 0
formBuilder/inputs/PortableText/PresenceCursors.spec.tsx ✅ Passed (Inspect) 13s 3 9 0
formBuilder/inputs/PortableText/Styles.spec.tsx ✅ Passed (Inspect) 26s 6 0 0
formBuilder/inputs/PortableText/Toolbar.spec.tsx ❌ Failed (Inspect) 2m 24s 20 0 1
formBuilder/tree-editing/TreeEditing.spec.tsx ✅ Passed (Inspect) 0s 0 3 0
formBuilder/tree-editing/TreeEditingNestedObjects.spec.tsx ✅ Passed (Inspect) 0s 0 3 0

Copy link
Contributor

github-actions bot commented Jan 28, 2025

⚡️ Editor Performance Report

Updated Thu, 30 Jan 2025 15:35:21 GMT

Benchmark reference
latency of sanity@latest
experiment
latency of this branch
Δ (%)
latency difference
article (title) 24.1 efps (42ms) 25.6 efps (39ms) -3ms (-6.0%)
article (body) 67.8 efps (15ms) 67.6 efps (15ms) +0ms (-/-%)
article (string inside object) 27.0 efps (37ms) 27.0 efps (37ms) +0ms (-/-%)
article (string inside array) 24.4 efps (41ms) 23.8 efps (42ms) +1ms (+2.4%)
recipe (name) 45.5 efps (22ms) 50.0 efps (20ms) -2ms (-9.1%)
recipe (description) 50.0 efps (20ms) 50.0 efps (20ms) +0ms (-/-%)
recipe (instructions) 99.9+ efps (6ms) 99.9+ efps (6ms) +0ms (-/-%)
synthetic (title) 18.9 efps (53ms) 19.2 efps (52ms) -1ms (-1.9%)
synthetic (string inside object) 20.0 efps (50ms) 20.0 efps (50ms) +0ms (-/-%)

efps — editor "frames per second". The number of updates assumed to be possible within a second.

Derived from input latency. efps = 1000 / input_latency

Detailed information

🏠 Reference result

The performance result of sanity@latest

Benchmark latency p75 p90 p99 blocking time test duration
article (title) 42ms 60ms 74ms 425ms 1036ms 11.5s
article (body) 15ms 17ms 20ms 270ms 417ms 5.8s
article (string inside object) 37ms 41ms 49ms 89ms 274ms 6.8s
article (string inside array) 41ms 43ms 48ms 88ms 279ms 7.0s
recipe (name) 22ms 25ms 29ms 52ms 0ms 8.3s
recipe (description) 20ms 20ms 22ms 48ms 0ms 4.8s
recipe (instructions) 6ms 8ms 8ms 13ms 0ms 3.2s
synthetic (title) 53ms 57ms 74ms 301ms 1024ms 23.0s
synthetic (string inside object) 50ms 51ms 54ms 262ms 587ms 8.2s

🧪 Experiment result

The performance result of this branch

Benchmark latency p75 p90 p99 blocking time test duration
article (title) 39ms 42ms 66ms 444ms 470ms 10.6s
article (body) 15ms 18ms 33ms 149ms 349ms 5.5s
article (string inside object) 37ms 39ms 48ms 210ms 142ms 6.7s
article (string inside array) 42ms 44ms 48ms 191ms 306ms 7.1s
recipe (name) 20ms 22ms 25ms 69ms 0ms 7.1s
recipe (description) 20ms 21ms 25ms 57ms 1ms 4.9s
recipe (instructions) 6ms 8ms 9ms 10ms 0ms 3.4s
synthetic (title) 52ms 55ms 59ms 269ms 692ms 11.6s
synthetic (string inside object) 50ms 53ms 56ms 125ms 980ms 8.2s

📚 Glossary

column definitions

  • benchmark — the name of the test, e.g. "article", followed by the label of the field being measured, e.g. "(title)".
  • latency — the time between when a key was pressed and when it was rendered. derived from a set of samples. the median (p50) is shown to show the most common latency.
  • p75 — the 75th percentile of the input latency in the test run. 75% of the sampled inputs in this benchmark were processed faster than this value. this provides insight into the upper range of typical performance.
  • p90 — the 90th percentile of the input latency in the test run. 90% of the sampled inputs were faster than this. this metric helps identify slower interactions that occurred less frequently during the benchmark.
  • p99 — the 99th percentile of the input latency in the test run. only 1% of sampled inputs were slower than this. this represents the worst-case scenarios encountered during the benchmark, useful for identifying potential performance outliers.
  • blocking time — the total time during which the main thread was blocked, preventing user input and UI updates. this metric helps identify performance bottlenecks that may cause the interface to feel unresponsive.
  • test duration — how long the test run took to complete.

@stipsan stipsan force-pushed the use-live-content-api branch from 6bdee9b to 084ad72 Compare January 28, 2025 12:45
@stipsan stipsan force-pushed the use-live-content-api branch from 084ad72 to 2cd609d Compare January 29, 2025 10:46
@stipsan stipsan force-pushed the use-live-content-api branch from 2cd609d to 39127ab Compare January 29, 2025 14:48
@stipsan stipsan marked this pull request as ready for review January 29, 2025 16:01
@stipsan stipsan requested review from a team as code owners January 29, 2025 16:01
@stipsan stipsan requested review from bjoerge and removed request for a team January 29, 2025 16:01
@stipsan stipsan enabled auto-merge January 29, 2025 16:01
@stipsan stipsan disabled auto-merge January 29, 2025 16:08
@stipsan stipsan force-pushed the use-live-content-api branch from 39127ab to 91cd5cb Compare January 29, 2025 20:53
Comment on lines -138 to -149
useEffect(() => {
setOnline(navigator.onLine)
// eslint-disable-next-line @typescript-eslint/no-shadow
const online = () => setOnline(true)
const offline = () => setOnline(false)
window.addEventListener('online', online)
window.addEventListener('offline', offline)
return () => {
window.removeEventListener('online', online)
window.removeEventListener('offline', offline)
}
}, [])
Copy link
Member Author

Choose a reason for hiding this comment

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

client.live.events() handles this internally by emitting a reconnect event.

Comment on lines -156 to -171
// Should pause activity when offline
if (!online) {
return true
}

// Should pause when the document isn't visible, as it's likely the user isn't looking at the page
if (visibilityState === 'hidden') {
return true
}

return false
}

function onVisibilityChange(onStoreChange: () => void): () => void {
document.addEventListener('visibilitychange', onStoreChange)
return () => document.removeEventListener('visibilitychange', onStoreChange)
Copy link
Member Author

Choose a reason for hiding this comment

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

client.live.events() does not handle pausing on visibility hidden, but it's less necessary since we're no longer polling either. If a tab is suspended and brought back we do however handle that automatically.

Comment on lines -114 to -116
const onFocus = () => setState('stale')
window.addEventListener('focus', onFocus)
return () => window.removeEventListener('focus', onFocus)
Copy link
Member Author

Choose a reason for hiding this comment

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

With client.live.events() there's no longer a need to refetch on window focus 🥳

Comment on lines -56 to -59
export function useQueryParams(params?: undefined | null | QueryParams): QueryParams {
const stringifiedParams = useMemo(() => JSON.stringify(params || {}), [params])
return useMemo(() => JSON.parse(stringifiedParams) as QueryParams, [stringifiedParams])
}
Copy link
Member Author

Choose a reason for hiding this comment

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

The stability of query params are now handled by the useLiveQueries reducer, we no longer need this hack 😌

@@ -227,7 +227,6 @@
"lodash": "^4.17.21",
"log-symbols": "^2.2.0",
"mendoza": "^3.0.0",
"mnemonist": "0.39.8",
Copy link
Member Author

Choose a reason for hiding this comment

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

One less chonky dependency 😮‍💨

}, [])

useEffect(() => {
useEffect((): (() => void) => {
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 typing:

useEffect((): (() => void) => {
           // ^
})

Is there to avoid easy mistakes, as further down this is returned:

return nextComlink.start()

Calling .start() returns an unsubscribe handler. This is a bit implicit and easy to miss. Should someone refactor this useEffect later and miss the return statement then TS will catch it 😮‍💨

Copy link
Member

Choose a reason for hiding this comment

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

Nice! In future I'll explicitly call unsubscribe in cleanup functions as I'm definitely guilty of not doing this, and I can see how it could be missed in a refactor.

key={`${key}${perspective}`}
projectId={clientConfig.projectId!}
dataset={clientConfig.dataset!}
key={`${liveEvents.resets}:${key}`}
Copy link
Member Author

Choose a reason for hiding this comment

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

The liveEvents.resets ensures that if a reconnect or restart live event happens then QuerySubscription components here start from a clean slate. This helps ensure their internal tracking of what liveEventsMessages they can ignore, as they happened before the component mounted, and which ones to look for a lastLiveEventId, works correctly.

@stipsan stipsan enabled auto-merge January 29, 2025 21:19
@@ -10,26 +11,13 @@ export const EDIT_INTENT_MODE = 'presentation'
export const MAX_TIME_TO_OVERLAYS_CONNECTION = 3_000 // ms

// The API version to use when using `@sanity/client`
export const API_VERSION = '2023-10-16'
export const API_VERSION = apiVersion
Copy link
Member Author

Choose a reason for hiding this comment

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

Aligning on the apiVersion used here increases the potential effect of sanity-io/client#990

Copy link
Member

@rdunk rdunk left a comment

Choose a reason for hiding this comment

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

Awesome, this is a great improvement and another great benefit of the LCAPI. It should also help alleviate the fear users have when they inspect the network tab when using Presentation. 😆

Thanks for all the comments, really helpful as usual.

@stipsan stipsan added this pull request to the merge queue Jan 30, 2025
Merged via the queue into next with commit 86e5af8 Jan 30, 2025
56 checks passed
@stipsan stipsan deleted the use-live-content-api branch January 30, 2025 16:40
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