Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/modern-mammals-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@portabletext/react": patch
---

Avoid repeated serialization of nested styled list items and reuse unchanged blocks
and prepared marks trees in the React Compiler build.

Reuse requires immutable block identities, stable keys, components and missing-component
handlers, and unchanged block indices. Mounts, refetched documents with new objects,
and updates with new component maps can incur additional per-block overhead.

Missing-component handlers no longer run for discarded styled-list subtrees or reused
block output. Each top-level block and list item now has a `RenderedNode` boundary
(including in the hook-free RSC build). Built-in block styles and list items render
native tags directly, so their default component functions no longer appear in React
DevTools or warning stacks. Custom components still receive the full props contract,
including mark text and the single-argument `renderNode` callback.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jspm_packages
# Compiled portable text library + demo
/dist
/demo/dist
/test/performance/dist

*.iml
.idea/
Expand Down
1 change: 1 addition & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"dist/**",
"coverage/**",
"demo/dist/**",
"test/performance/dist/**",
".nyc_output/**",
"test/typegen/sanity.types.ts",
// The Next.js integration fixture resolves `next` types from its own node_modules, which
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Migrating from [@sanity/block-content-to-react](https://www.npmjs.com/package/@s
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [React Server Components](#react-server-components)
- [Rendering performance](#rendering-performance)
- [Styling](#styling-the-output)
- [Customizing components](#customizing-components)
- [Available components](#available-components)
Expand Down Expand Up @@ -50,6 +51,14 @@ import {PortableText} from '@portabletext/react'

`<PortableText>` renders in both worlds without configuration: use it directly in a React Server Component, or in a file with a `'use client'` directive. The package publishes two builds selected automatically through export conditions — client components and SSR load a build optimized with [React Compiler](https://react.dev/learn/react-compiler), while server components load an uncompiled build through the `react-server` condition ([React Compiler output can't load in server components](https://github.com/facebook/react/issues/31702), and since server components render exactly once, memoization couldn't pay off there anyway).

## Rendering performance

The compiled build can reuse unchanged blocks when the containing `value` array changes. Treat values as immutable: replace edited blocks and their children, retain unchanged block objects, and use stable `_key` values. Keep `components` and `onMissingComponent` referentially stable as well. Recreating every block (for example after a refetch) or passing a new `components` object forfeits rendered-output reuse. A block's index must also stay the same: inserting or deleting a block rerenders subsequent blocks whose indices shift.

Marks-tree preparation is independent of presentation, so changing components can reuse prepared content. List nesting still processes a changed document, and generated list containers and items containing nested lists may need rebuilding. Context consumers inside custom components continue to update normally. Missing-component callbacks are not repeated for reused output. The server-component build uses the same rendering fast paths without React memoization hooks or cross-request caches.

Reuse trades per-block bookkeeping and component boundaries for cheaper structurally shared updates. It can slow simple paragraph/list mounts, fully recreated documents, and updates with new component maps; these costs also matter for read-only CMS content. See the [benchmark guide](test/performance/README.md) to measure the full matrix on your workload rather than assuming a universal speedup.

## Styling the output

The rendered HTML does not have any styling applied, so you will either render a parent container with a class name you can target in your CSS, or pass [custom components](#customizing-components) if you want to control the direct markup and CSS of each element.
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@
},
"scripts": {
"analyze": "ENABLE_BUNDLE_ANALYZER=true pnpm build",
"bench": "pnpm build && NODE_ENV=production vitest bench --run",
"bench:browser": "vite preview test/performance --config test/performance/vite.config.ts",
"build": "tsdown",
"build:bench": "pnpm build && vite build test/performance --config test/performance/vite.config.ts --emptyOutDir",
"prebuild:demo": "pnpm clean",
"build:demo": "vite build demo --config=./vite.config.demo.ts --base=/react-portabletext/",
"clean": "rimraf dist coverage demo/dist .nyc_output",
"clean": "rimraf dist coverage demo/dist test/performance/dist .nyc_output",
"dev": "vite demo --config=./vite.config.demo.ts",
"format": "oxfmt .",
"lint": "oxlint --type-aware --type-check --deny-warnings --report-unused-disable-directives",
Expand Down Expand Up @@ -73,6 +76,7 @@
"esbuild": "^0.28.2",
"esbuild-register": "^3.6.0",
"groq": "^6.12.0",
"jsdom": "29.1.1",
"leaflet": "^1.9.4",
"oxc-transform-react": "^0.147.0",
"oxfmt": "^0.66.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading