sanity-io / sanity

Sanity Studio – Rapidly configure content workspaces powered by structured content
https://www.sanity.io
MIT License
5.18k stars 418 forks source link

Laggy text inputs #5226

Open piscopancer opened 10 months ago

piscopancer commented 10 months ago

SmartSelect_20231117-105841_Chrome.jpg

When I type in the text input, there is a 0.5 seconds delay before I see any updates.

What information should I provide you to figure out what's causing the lag? Least I can say is that my app runs on Next 14.

bjoerge commented 10 months ago

What information should I provide you to figure out what's causing the lag?

What version of Studio, browser, CPU etc would be helpful, thanks!

piscopancer commented 10 months ago

@bjoerge sanity version is 3.20.0, CPU is AMD Ryzen 5 (Cyberpunk 2077 medium graphics 40-60fps), the browser is Yandex Browser. In fact, inputs lag both on mobile (google chrome) and my laptop (yandex browser), I suppose the problem must be related to my project, not hardware.

Do you have any assumptions what can cause the input lag? Initially, I thought it happens because an input sends a request to a sanity server to write new value, and updates itself only after the server responds with updated value, in other words, with success. 🤔

    "dependencies": {
        "@mdx-js/loader": "^3.0.0",
        "@next/mdx": "^14.0.3",
        "@radix-ui/react-dialog": "^1.0.5",
        "@radix-ui/react-popover": "^1.0.7",
        "@radix-ui/react-toast": "^1.1.5",
        "@radix-ui/react-tooltip": "^1.0.7",
        "@sanity/ui": "^1.9.3",
        "@sanity/vision": "^3.20.0",
        "@studio-freight/lenis": "^1.0.27",
        "@svgr/webpack": "^8.1.0",
        "@types/node": "20.9.1",
        "@types/react": "^18.2.37",
        "@types/react-dom": "^18.2.15",
        "autoprefixer": "10.4.16",
        "date-fns": "^2.30.0",
        "easymde": "^2.18.0",
        "eslint": "8.54.0",
        "eslint-config-next": "^14.0.3",
        "framer-motion": "^10.16.5",
        "motion": "^10.16.4",
        "next": "^14.0.3",
        "next-mdx-remote": "^4.4.1",
        "next-sanity": "^6.0.5",
        "postcss": "8.4.31",
        "react": "^18.2.0",
        "react-dom": "^18.2.0",
        "react-icons": "^4.12.0",
        "react-markdown": "^9.0.1",
        "react-refractor": "^2.1.7",
        "react-use-precision-timer": "^3.3.1",
        "refractor": "^4.8.1",
        "rehype-react": "^8.0.0",
        "remark-gfm": "^4.0.0",
        "remark-unwrap-images": "^4.0.0",
        "sanity": "^3.20.0",
        "sanity-plugin-markdown": "^4.1.0",
        "sass": "^1.69.5",
        "tailwindcss": "3.3.5",
        "transliteration": "^2.3.5",
        "typescript": "5.2.2",
        "valtio": "^1.12.0"
    },
    "devDependencies": {
        "@tailwindcss/typography": "^0.5.10",
        "prettier": "^3.1.0",
        "prettier-plugin-tailwindcss": "^0.5.7"
    }
hallatore commented 10 months ago

I have the same issue with sanity in next. Here is a performance profile while I type some text. I first thought it had something to do with preview. but this happens without any preview provider enabled.

Some things I noticed:

performance profile

image image

(Unusual?) network traffic

Not sure if normal, but a single character triggers quite a lot of network calls. (1 post and 4ish get's). When I type it looks like this.

image

Post call

{
    "mutations": [{
        "createIfNotExists": {
            "_id": "drafts.64a7f1c4-0f6b-42c6-8c5c-bf9eda5cbddd",
            "_type": "carConfigurationPage",
            "_createdAt": "2023-11-23T11:29:17Z",
            "carTypes": [{
                "_key": "6b853240c73e",
                "_ref": "7cDM6ekVZIUb0JKOB5sk3E",
                "_type": "reference"
            }, {
                "_key": "644c8eb221e6",
                "_ref": "dbb3db34-6e66-40dd-9626-df3ae159634d",
                "_type": "reference"
            }],
            "sections": [{
                "_key": "4f18f9b04afc",
                "_type": "carConfigurationSection",
                "content": [{
                    "_key": "ab0967ff4863",
                    "_type": "textContent",
                    "content": "Intro om farge og slikt"
                }, {
                    "_key": "73ed2c959ab4",
                    "_type": "carOptionsContent",
                    "key": "farge"
                }],
                "title": "Farge"
            }, {
                "_key": "ab6bc49f9ce2",
                "_type": "carConfigurationSection",
                "content": [{
                    "_key": "9fae8bfa67c2",
                    "_type": "textContent",
                    "content": "Intro om felger"
                }, {
                    "_key": "11335e856cfc",
                    "_type": "carOptionsContent",
                    "key": "felger"
                }, {
                    "_key": "fac3a54608fe",
                    "_type": "carOptionsContent",
                    "key": "vinterhjul"
                }],
                "title": "Felger"
            }],
            "title": "Born 62"
        }
    }, {
        "patch": {
            "setIfMissing": {
                "sections": []
            },
            "id": "drafts.64a7f1c4-0f6b-42c6-8c5c-bf9eda5cbddd"
        }
    }, {
        "patch": {
            "setIfMissing": {
                "sections[_key==\"ab6bc49f9ce2\"]": {
                    "_type": "carConfigurationSection"
                }
            },
            "id": "drafts.64a7f1c4-0f6b-42c6-8c5c-bf9eda5cbddd"
        }
    }, {
        "patch": {
            "setIfMissing": {
                "sections[_key==\"ab6bc49f9ce2\"].content": []
            },
            "id": "drafts.64a7f1c4-0f6b-42c6-8c5c-bf9eda5cbddd"
        }
    }, {
        "patch": {
            "setIfMissing": {
                "sections[_key==\"ab6bc49f9ce2\"].content[_key==\"9fae8bfa67c2\"]": {
                    "_type": "textContent"
                }
            },
            "id": "drafts.64a7f1c4-0f6b-42c6-8c5c-bf9eda5cbddd"
        }
    }, {
        "patch": {
            "id": "drafts.64a7f1c4-0f6b-42c6-8c5c-bf9eda5cbddd",
            "diffMatchPatch": {
                "sections[_key==\"ab6bc49f9ce2\"].content[_key==\"9fae8bfa67c2\"].content": "@@ -20,60 +20,5 @@\n s%C3%A5nt\n- her what is this about? some more text that is comming\n"
            }
        }
    }],
    "transactionId": "5da59b76-79ee-47c9-bcd9-a2db822d5f2f"
}

Next project package.json

"dependencies": {
    "@datadog/browser-logs": "^5.4.0",
    "@datadog/browser-rum": "^5.4.0",
    "@moller/design-system": "^6.11.0",
    "@portabletext/react": "^3.0.11",
    "@sanity/client": "^6.8.6",
    "@sanity/image-url": "^1.0.2",
    "@sanity/overlays": "^2.0.2",
    "@sanity/preview-kit": "^4.0.1",
    "@sanity/vision": "^3.20.1",
    "@types/node": "20.9.4",
    "@types/react": "18.2.38",
    "@types/react-dom": "18.2.17",
    "autoprefixer": "10.4.16",
    "classnames": "^2.3.2",
    "eslint-config-next": "14.0.3",
    "groq": "^3.20.1",
    "jotai": "^2.5.1",
    "next": "14.0.3",
    "next-sanity": "^6.0.5",
    "postcss": "^8.4.31",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-icons": "^4.12.0",
    "sanity": "^3.20.1",
    "sanity-plugin-iframe-pane": "^2.6.1",
    "sanity-plugin-media": "^2.2.4",
    "suspend-react": "^0.1.3",
    "swr": "^2.2.4"
}
bjoerge commented 9 months ago

We landed a couple of performance optimizations in v3.20.2, which should make things significantly faster. Could you try upgrading and see if that helps?

hallatore commented 9 months ago

@bjoerge installed and built it. Not much difference.

I can see that each keypress event trigger is ~45 ms long.

Video: https://github.com/sanity-io/sanity/assets/365605/6851ce41-707e-43dc-955c-47f6a9076894 image

hallatore commented 9 months ago

@bjoerge Looks like something in react hooks into keypress and triggers on every input.

image

hallatore commented 9 months ago

A look at reacts profiler plugin for chrome it looks like the whole sanity studio component is re-rendering on keypress

hallatore commented 9 months ago

Screenshot from react profile on which component that re-renders on keypress.

image

hallatore commented 9 months ago

Here is a video displaying the issue. The whole document pane re-renders on keypress.

https://github.com/sanity-io/sanity/assets/365605/9631adc4-952a-4013-b2d8-45daecbb3704

hallatore commented 9 months ago

I created a new sanity project and added a single schema. I see a 14 ms re-render on each keypress.

Steps to reproduce

  1. Create a new sanity project using https://www.sanity.io/docs/create-a-sanity-project
  2. In schema/index.ts add the following
    
    import {defineField, defineType} from 'sanity'

const textContent = defineType({ name: 'textContent', title: 'Text content', type: 'document', fields: [ defineField({ name: 'name', title: 'Name', type: 'string', }), defineField({ name: 'content', title: 'Content', description: 'Text content', type: 'text', }), ], })

export const schemaTypes = [textContent]



3. Open studio, create a new document and start typing in the content text area. Run profiler og react profiler to record performance.
hallatore commented 9 months ago

Hi again. After looking at the code I see that each field is memorized for performance. I see 2 issues with todays solution.

  1. For a complex field the whole field is re-rendered. Under are two performance traces, one for a string input, and one for a complex field. Each profile is for a single input character.
  2. The input fields need some sort of debounce/throttle. Right now the profile traces below happen for every single keyboard input. I don't see a need to propagate onChange on every single character. It would also reduce a ton of server requests as each single input also triggers a lot of requests.

Simple string field: 1 character input image

Complex object field: 1 character input image

Complext object structure

defineField({
    name: 'sections',
    title: 'Sections',
    description: 'Page sections',
    type: 'array',
    of: [{ type: carConfigurationSection }],
}),

export default defineType({
    name: carConfigurationSection,
    title: 'Section',
    type: 'object',
    fields: [
        defineField({
            name: 'title',
            title: 'Title',
            type: 'string',
        }),
        defineField({
            name: 'content',
            title: 'Content',
            description: 'Section content',
            type: 'array',
            of: [{ type: textContent }, { type: carOptionsContent }],
        }),
    ],
});

export default defineType({
    name: textContent,
    title: 'Text content',
    type: 'object',
    fields: [
        defineField({
            name: 'name',
            title: 'Name',
            type: 'string',
        }),
        defineField({
            name: 'content',
            title: 'Content',
            description: 'Text content',
            type: 'text',
        }),
    ],
});
...
bjoerge commented 9 months ago

@hallatore thanks for taking the time to provide details. We are well aware of how the changed propagate from text inputs back to the UI, but it works this way for a reason. Debouncing inputs is generally something we want to avoid due to the complexity it adds (correctness trumps performance).

To be clear: I'm not disagreeing with your points, nor am I saying it shouldn't be faster, but we're a small team with lots of other priorities, so we're aiming for acceptable, not perfect when it comes to performance. I'm curious whether this is causing real issues for you or your editors in production, or if we're moving into "it would be nice if it was faster"-territory here. 14ms is comfortably above 60fps, and there's hardly anyone in this world that type at a rate that actually requires 60fps.

Also keep in mind that (as you probably already know) there will be a huge added perf tax running locally in dev mode, esp. with devtools open and in particular with rerender highlighting enabled, so please keep that in mind.

You said there was "Not much difference" after upgrading to 3.20.3 - are you able to quantify this? Our measurements showed a pretty significant difference.

If you can provide a concrete repro case of something that is too slow for it to practically work for an editor in production we're happy to look into it.

hallatore commented 9 months ago

@bjoerge

Did a re-test with older versions yesterday. Upgrading had no impact. Same impact with latest or year old packages.

Did a test in production now just to make sure I'm not in dev. When I edit the text-input, in the complex example above, I see a 70 ms freeze per keystroke.

I'll see if I can make a repro case later that showcases the issues we are seeing in the project.

I agree with you on the correctness trumps performance part. If debouncing would be introduced then it has to be "invisible" to the user experience.


Here is a psudo example of how I think it could be used (not that it necessary should) In this example the onChange would fire 500 ms after I stop typing AND every 1 second if I type a lot of text really fast. It would ensure only 1 70 ms update every second, and preferably an update not while the user is typing.

... debounce(() => {
        triggerOnChange(...);
    },
    500,
    { maxWait: 1000 }
);
hallatore commented 9 months ago

This issue is a bit hard to reproduce consistently. But I think I figured out why.

The issue is single core cpu bound

A fast enough pc doesn't seem to show much lag. If the single core performance is good enough.

There is a "threshold"

When testing, it seems stuff is fine for some pc, but laggy for another. When a pc is slow enough the feeling of lag much more pronounced. I even noticed this one time with my laptop simply because it didn't turbo the CPU that time. That was enough on that laptop to make the lag visible.

How to emulate

You can emulate this by turning on cpu thottling in chrome under performance tab. Stuff should work slower with this on, but typing on my laptop now takes 1 second to update.

image

hallatore commented 9 months ago

Proof of concept for TextInput.tsx that doesn't update on every keystroke.

Here is CPU usage of me typing This is me typing hello world in sanity cms on current and test build. (The test-build saved 3 times while I wrote)

image

import {ChangeEventHandler, ChangeEvent, useState, useEffect, FormEvent} from 'react'
import {TextSchemaType} from '@sanity/types'
import {TextArea} from '@sanity/ui'
import styled from 'styled-components'
import {StringInputProps} from '../types'

/**
 *
 * @hidden
 * @beta
 */
export type TextInputProps = StringInputProps<TextSchemaType>

const StyledTextArea = styled(TextArea)`
  &[data-as='textarea'] {
    resize: vertical;
  }
`

function createProxyChangeEvent(value: string): FormEvent<HTMLTextAreaElement> {
  const dummyTextArea = document.createElement('textarea')
  dummyTextArea.value = value

  const customEvent = new Event('change', {
    bubbles: true,
  }) as unknown as ChangeEvent<HTMLTextAreaElement>

  // Set both the target and currentTarget of the event to be the dummy textarea
  Object.defineProperty(customEvent, 'target', {
    writable: false,
    value: dummyTextArea,
  })
  Object.defineProperty(customEvent, 'currentTarget', {
    writable: false,
    value: dummyTextArea,
  })

  return customEvent
}

/**
 *
 * @hidden
 * @beta
 */

export function TextInput(props: TextInputProps) {
  const {schemaType, validationError, elementProps} = props
  const {value, onChange, ...extraProps} = elementProps
  const [inputValue, setInputValue] = useState(value || '')

  // Todo: Handle if the value prop updates with something that didn't come from the input itself

  const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
    const newValue = e.currentTarget.value
    setInputValue(newValue)
  }

  useEffect(() => {
    const timeout = setTimeout(() => {
      onChange(createProxyChangeEvent(inputValue))
    }, 500)

    return () => {
      clearTimeout(timeout)
    }
  }, [inputValue, onChange])

  return (
    <StyledTextArea
      customValidity={validationError}
      value={inputValue}
      onChange={handleChange}
      placeholder={schemaType.placeholder}
      rows={typeof schemaType.rows === 'number' ? schemaType.rows : 10}
      {...extraProps}
    />
  )
}
hallatore commented 9 months ago

@piscopancer If you test with chrome. What does the CPU usage under Performance monitor tools go to?

On my pc the lagging is less obvious if it happens to stay below 100% while I type.

image

JohnGemstone commented 8 months ago

Hey there, running into similar issue, reproducible using the example template-nextjs-personal-website

Once installed test the text input for the timeline components in the project document schema from the structure panel: image

image

As you would expect, the lag is worse in the presentation panel:

image

I found that nested inputs lag exponentially depending on how deep they are nested. Responsiveness for normal top level string inputs are great though.

Just thought I'd share, I'm a big user of page builders which sometimes have up to four levels of nesting, which unfortunately means a sluggish editor experience.

Judging by https://github.com/sanity-io/sanity/pull/5270 this should be fixed, is it possible something has regressed? When I get a moment ill try installing v3.20.2

FYI this is running on an M1Pro using Sanity 3.23.4, Chrome

EDIT: My issue was testing on a dev server, deployed the text inputs are really fast. Great work thank you!

OscarGodson commented 6 months ago

Text for me is super laggy. It's so laggy it feels distracting and if you typo something you don't notice for a couple seconds until the letters all render. It sort of is the feeling of when you hear yourself echoing on a call but have to keep talking lol.

The video below looks like a slow frame rate but it's actually what I see exactly. I type at least 75-80WPM and you see the letters display in groups but really it should be a smooth letter by letter input and no more than like 1s (how fast I type hello world). I finish typing for nearly 2s before what I typed has been fully displayed.

https://github.com/sanity-io/sanity/assets/20615/04a53d5e-1d69-45c4-8ad3-502354aed312

I'm using Edge 122.0.2365.80 on Mac and Next Sanity 8.0.0 and Sanity Vision 3.29.1

And I'm on a 2.3 GHz 8-Core Intel Core i9 with 32GB of RAM so my computer should be able to handle it fine.

XavierMod commented 4 months ago

Getting this exact same issue. All fields within Structure are very slow. Are there any updates on this?

JohnGemstone commented 4 months ago

As mentioned in my comment, make sure you arent running sanity from a development server

XavierMod commented 4 months ago

Thanks for the quick response. What would the alternative be if I'm working on a site that's not ready to go live yet? I'm running the studio within a next.js project that gets served when doing npm run dev. My setup's similar to this: https://www.sanity.io/blog/build-your-own-blog-with-sanity-and-next-js

JohnGemstone commented 4 months ago

You could always try npm run build then npm run start to use a production server 👍

snozwoz commented 2 months ago

We have same issue, I have added more information in community slack at this URL https://sanity-io-land.slack.com/archives/C9Z7RC3V1/p1721316378567009

The issue is related to a vast number of API calls- we counted 44 for entering 7 characters.

Does anyone have further updates or solutions to this - we have spent many developer days looking at this and trying everything we can think of, including setting up a new project from the example code base, just to be sure it's not something we are doing.

We have made some improvements by adding a debounce wrapper but have not resolved the underlying issue.