React アプリケーションのレンダリングを高速化する方法

Nov 27 2022
アプリケーションを高速に配信するのは簡単ですが、高速なアプリケーションを配信するのは非常に困難です。ここでは、React アプリケーションでコンポーネントをより高速にレンダリングするためのヒントをいくつか紹介します。

アプリケーションを高速に配信するのは簡単ですが、高速なアプリケーションを配信するのは非常に困難です。ここでは、React アプリケーションでコンポーネントをより高速にレンダリングするためのヒントをいくつか紹介します。

私たちはアプリケーションをより迅速に提供するよう努めていますが、ベスト プラクティスに焦点を当てることを忘れがちです。しばらくすると、非常に複雑なコンポーネントが作成されます。キーを入力すると、レンダリングする必要のない場所がレンダリングされます。ここでは、何をすべきか、何をすべきでないかのリストを示します。

React.Memo を使う

通常、親がレンダリングされるたびにコンポーネントをレンダリングすることは望ましくありません。必要な場合を除き、React はコンポーネントをレンダリングしないと言えます。

// Wrong usage
// Without React.memo this component will get rendered each time 
// something causes its parent to render
const YourHeavyComponent = (name, title, onClick) => {
    // Render  something really heavy like whole page here
}

export default YourHeavyComponent
// Good usage
// This component will not render unless name, title or onClick changes
const YourHeavyComponent = (name, title, onClick) => {
    // Render  something really heavy like whole page here
}

export default React.memo(YourHeavyComponent)
// This component will not render unless title changes
const YourHeavyComponent = (name, title, onClick) => {
    // Render  something really heavy like whole page here
}

// This usage might better for some stations, use this second argument
// to specify which props  will cause render
export default React.memo(YourHeavyComponent, (prevProps, nextProps) => {
    return prevProps.title === nextProps.title
})

コンポーネントごとに 4 つを超える props を持たないでください

多くの props を持つことは常に問題を引き起こします。各 props は、パフォーマンスに関して考慮すべき変数が 1 つ増えることを意味し、コードの読み取りと保守が難しくなります。10 個の props を持つコンポーネントを作成する代わりに、3 ~ 4 個の props を持つ 3 つの小さなチャンクを作成します。

// this component has too many filters
// it's not a good idea to add filters inside this component
// instead we need to create a seperate component to add filter inputs
// and we can also remove title from here
const List = (title, items, sortBy, keys, filters, onFilterChange, children, pagination) => {
    // Render  something really heavy like whole page here
}
// here userslist wont rendered unless we get a new list
// pagination payload won't cause rendering in filters
const [payload, setPayload] = useState()
const [list, setList] = useState([])
return <>
    <Filters onFilterChange={setPayload} filters={payload.filters} />
    <UserList items={list}/>
    <Pagination onPaginationChange={setPayload} pagination={payload.pagination} />
</>

何かが変更された後に計算される変数がある場合にフックします。計算に時間がかかる場合、または計算後に配列またはオブジェクトの異なるメモリ参照が取得されるため、これは役立ちます。

変数が変更されて子に渡されると、子コンポーネントが再レンダリングされることを忘れないでください。

// here we use useMemo for purely caching purposes, if we don't use useMemo
// each time this components rendered the calculation will have to re-run 
const heavyCalculated = useMemo(() => doSomeHeavyCalculation(variable1), [variable])
// an example of wrong usage
// each time this components get rendered this
// styles variable will point to different memory address
// and this will cause rerender of YourHeavyComponent
const styles = {
  container:{
    marginLeft: left,
    marginRight: right,
    marginTop: top,
    marginBottom: bottom,
  }
}
// correct usage of useMemo
// this will cache your value and its memory point won't change
// so even if this components gets rendered your YourHeavyComponent won't be rendered again
const styles = useMemo(() => ({
  container:{
    marginLeft: left,
    marginRight: right,
    marginTop: top,
    marginBottom: bottom,
  }
}), [left, right, top, bottom])

関数を子コンポーネントに渡す場合は、useCallback フックを使用する必要があります。コンポーネントがレンダリングされるたびに、関数が異なるメモリ ポイントを指すことを忘れないでください。前のコールバックをメモリに保持する必要があるため、メイン コンポーネントで何かを実行して子でレンダリングを行うたびに変更されません。

// wrong usage
// each time App components get rendered for some reason
// onClickCallback will be recreated and it will point to different memory address
// so it will cause YourHeavyComponent to re render
const onClickCallback = () => {
  // do some stuff
}

return <div>
  <YourHeavyComponent onClick={onClickCallback} />
</div>

// good usage of onClickCallback
// each time App components if your variable1 don't change
// onClickCallback will point to same memory point 
// so UourHeavyCallback won't render.
const onClickCallback = () => {
  // do some stuff
}

return <div>
  <YourHeavyComponent onClick={onClickCallback} />
</div>

このフックを使用して子に渡し、そのインスタンスを使用することに慣れましたが、これは利用可能なユースケースだけではありません。再レンダリングを引き起こすことなく、ある種のキャッシングおよび状態メカニズムとして使用できます。

// in this example we look for useRef value instead of setting
// sent reference is being used for checking if we send this message before or not
// this could hold in the state but if we put into state it will cause render
// it into state, because we don't want to re-render after we set sent.current
const sent = useRef(false)
const postMessage = useCallback(() => {
  if (!sent.current) {
    // make your api call here
    sent.current = true
  }
}, [])

これを決して忘れないでください。コンポーネントをレンダリングするたびに、そのコンポーネント内の変数は異なるメモリアドレスを指します。文字列や整数などのプリミティブ型の場合、これは問題を引き起こしませんが、配列、オブジェクト、および関数を操作している場合(各関数は実際にはボンネットの背後にあるオブジェクトであるため)。いくつかの例を見てみましょう

const InlineTest = () => {
    // the usage below will cause YourHeavyComponent to render if
    // something causes InlineTest component to render
    return <YourHeavyComponent style={{
        marginLeft: 10,
        marginRight: 10
    }} />
}
const style = {
    marginLeft: 10,
    marginRight: 10
}

// We move styles outside of component
// this way style will point the same memory address regardless of 
// how many tames InlineTest gets rendered

const InlineTest = () => {
    // the usage below will cause YourHeavyComponent to render if
    // something causes InlineTest component to render
    return <YourHeavyComponent style={style} />
}

const InlineTest = () => {
    // the usage below will cause YourHeavyComponent to render if
    // something causes InlineTest component to render
    // because onClick will be assigned to a new function each time
    return <YourHeavyComponent onClick={() => {
        console.log('clicked');
    }} />;
}
import {useCallback} from "react";

const InlineTest = () => {
    // the usage below is correct way to do it
    // using useCallback will make sure onClick function
    // is the same between renders and it won't cause render in below component
    
    const onClick = useCallback(() => {
        console.log('clicked');
    }, [])
    return <YourHeavyComponent onClick={onClick} />;
}

反応でフォームを処理するのは簡単だと思うかもしれませんが、そうではないことを保証させてください。フォームが複雑になり、いくつかの検証などを行う必要がある場合、レンダリングが多すぎます。そのため、優れたライブラリを使用することが重要です。ここ数年、私はさまざまなライブラリを試しましたが、ここで言及する価値があるのは 1 つだけです。反応フックフォーム。

import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const onSubmit = data => console.log(data);


  return (
    /* "handleSubmit" will validate your inputs before invoking "onSubmit" */
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* register your input into the hook by invoking the "register" function */}
      <input defaultValue="test" {...register("example")} />
      
      {/* include validation with required or other standard HTML validation rules */}
      <input {...register("exampleRequired", { required: true })} />
      {/* errors will return when field validation fails  */}
      {errors.exampleRequired && <span>This field is required</span>}
      
      <input type="submit" />
    </form>
  );
}