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

docs(performance): new section #1208

Merged
merged 14 commits into from
Dec 9, 2022
Merged

docs(performance): new section #1208

merged 14 commits into from
Dec 9, 2022

Conversation

TwistedMinda
Copy link
Collaborator

@TwistedMinda TwistedMinda commented Jun 3, 2022

Following-up of this issue, we decided to:

  • mention in core that extra re-renders are expected behaviour (especially with React 18) (taken care of in this PR)
  • create a separate section to talk about Performance and preventing re-renders in general (taken care of in the present PR)

@vercel
Copy link

vercel bot commented Jun 3, 2022

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

Name Status Preview Updated
jotai ✅ Ready (Inspect) Visit Preview Dec 9, 2022 at 0:12AM (UTC)

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jun 3, 2022

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 43019bf:

Sandbox Source
React Configuration
React Typescript Configuration
React Browserify Configuration
React Snowpack Configuration
Next.js Configuration
Next.js with custom Babel config Configuration
React with custom Babel config Configuration

@dai-shi
Copy link
Member

dai-shi commented Jun 4, 2022

Hi, thanks for the suggestion and your work.
While what is written so far is correct, I feel like there would be a better way of structuring this guide if we were to cover render optimization technique with jotai.

Let's spend time on this. I will think about it and let you know the basic idea. I'm sure you will be able to extend it and write a great guide.

@TwistedMinda TwistedMinda marked this pull request as draft June 4, 2022 00:31
@TwistedMinda
Copy link
Collaborator Author

@dai-shi Yeah sure, forgot to set it as draft, but it's totally a first step. I need help to know if I'm going on the good way. Looking forward to discuss that with you. Gonna bring some changes from time to time until we get something decent.

We're in no rush to have this new page ;) Also probably the topic that most interests me!

@TwistedMinda TwistedMinda changed the title docs(performance): created as a separate section docs(performance): new section Jun 7, 2022
@dai-shi
Copy link
Member

dai-shi commented Jun 7, 2022

I will think about it

Here's rough idea how I would like to have jotai performance guide to be.

Jotai performance guide

Optimizing re-render

Atoms are the unit of triggering re-renders, so atoms should be small enough to reduce extra re-renders

// bad practice
const objAtom = atom({ x: 0, y: 0 })
const Component1 = () => {
  const [value, setValue] = useAtom(objAtom) // this will trigger re-renders either `x` or `y` changes
  ...
}

// good practice
const xAtom = atom(0)
const yAtom = atom(0)
const Component2 = () => {
  const [x, setX] = useAtom(xAtom)
  const [y, setY] = useAtom(yAtom)
  ...
}

That's general guideline, but there are exceptions. If you always use xAtom and yAtom together, the combined objAtom is better because useAtom creates a subscription which is an overhead (which is far smaller than an extra re-render).

Likewise, derived atoms (custom read) are re-evaluated when dependency atoms change

// bad practice
const objAtom = atom({ x: 0, y: 0 })
const derived1Atom = atom((get) => get(objAtom).x * 2) // extra evaluation when only `y` changes

// good practice
const xAtom = atom(0)
const yAtom = atom(0)
const derived2Atom = atom((get) => get(xAtom) * 2)

Using big atoms

Sometimes, we want to create an atom with large object in it. For example, if you use atomWithStorage or an atom to be sync with server data.

In this case, you can create derived atoms to reduce extra re-renders.

const objAtom = atom({ x: 0, y: 0 })
const xAtom = atom((get) => get(objAtom).x)
const Component = () => {
  const [x, setX] = useAtom(xAtom)
  ...
}

You can also make the xAtom writable with write function.
Some additional functions such as focusAtom and splitAtom help this use case.

Creating atoms dynamically

Creating atoms with the atom function is very lightweight.
So, feel free to create an atom and throw it away.
When we create an atom in React render function (components/hooks), we should use useMemo or something. Otherwise, useAtom may cause infinite loop.

const objAtom = atom({ x: 0, y: 0 })
const xAtom = atom((get) => get(objAtom).x)
const Component = ({ isX }) => {
  const derivedAtom = useMemo(
    () => atom((get) => get(objAtom)[isX ? 'x' : 'y']),
    [isX]
  )
  const [value, setValue] = useAtom(derivedAtom)
  ...
}

Notes about concurrent rendering

  • read function of derived atoms are evaluated in the render phase.
  • if read function takes time, concurrent rendering may throw away the result, without commits (without browser paints).
  • write function of derived atoms are evaluated in sync.

Notes about render without commits

  • jotai uses useReducer internally, and it's often the case derived atoms trigger re-renders without commits (no browser paints).
  • If this can be an issue, you want to consider splitting dependency atoms, adding intermediate atoms, or memoize function.

(Need code snippets?)

Notes about Suspense

Async read function in derived atoms is very easy to use Suspense.
But, because read function is evaluated in the render phase, it can be too late to start the async function, especially for data fetching. This can lead waterfalls and the performance gets really bad.

We are still working on best practices, but for now two approaches are:

  1. start multiple async read at once in a higher component in the tree. usePrepareAtoms is available in jotai-suspense.
  2. start async functions in write, and set a promise as an atom value. This will ideal performance-wise, but you need to know all dependent atoms and update all at once. (It doesn't seem very practical in a complex app.)

General React techniques

  • use useMemo to avoid re-evaluate heavy computation
  • React.memo is useful for item components for an array (even if we use "atoms-in-atom" pattern. However, don't overuse React.memo as, for many cases, atoms are enough to optimize re-renders.

@@ -104,8 +104,9 @@ Then we use it in our component:
import { atom, useAtom } from 'jotai'
import { selectAtom, splitAtom } from 'jotai/utils'

const Tags: React.FC = () => {
const tagsAtom = selectAtom(readOnlyInfoAtom, (s) => s.tags)
const selector = (s) => s.tags
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const selector = (s) => s.tags
const selectTags = (s) => s.tags

How's this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@dai-shi Yeah or tagsSelector, both ok to me

@dai-shi dai-shi marked this pull request as ready for review December 9, 2022 12:13
@dai-shi
Copy link
Member

dai-shi commented Dec 9, 2022

While this guide is still fairly WIP, let's merge it and get feedback.
fwiw, my draft doesn't look very good now, because some of the limitations will be solved in v2.

@dai-shi dai-shi merged commit d5d04a0 into pmndrs:main Dec 9, 2022
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