본문 바로가기

FrontEnd

[Epic React][Build an Epic React App][Performance]

반응형

bookshelf/INSTRUCTIONS.md at exercises/09-performance · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

Background


웹 애플리케이션이 직면하는 가장 일반적인 성능 문제 중 하나는 초기 페이지 로드입니다.
애플리케이션이 커짐에 따라 JavaScript 번들의 크기도 커집니다.
인터넷을 통해 해당 데이터를 보내는 데 시간이 더 오래 걸리고
브라우저가 해당 데이터를 처리하는 데 더 많은 시간이 걸리기 때문에 성능에 부정적인 영향을 미칩니다.
(예: JavaScript 구문 분석 및 실행).
그러나 애플리케이션이 아무리 커지더라도 사용자는 애플리케이션이 페이지에서 동시에 수행할 수 있는 모든 것을 필요로 할 것 같지 않습니다.
따라서 대신 애플리케이션 코드와 데이터를 논리적 "청크"로 분할하면 사용자가 지금 당장 하고 싶은 일에 필요한 청크만 로드할 수 있습니다.
사용자가 원하는 콘텐츠를 빨리 볼 수 있을수록 더 좋으며 이는 이를 측정하기 위한 지표입니다.

이 페이지의 목표는 time to first meaningful paint를 개선하는 것입니다.

 

 

실습


코드를 논리적 단위로 분할하기 (페이지) => 코드 스플리팅

  • Lazy Loading할 블록을 Suspense로 감싼다
    • fallback은 로딩 보여주는 컴포넌트임
    • 오류는 ErrorBoundary 사용
  • React.lazy로 코드를 비동기로 가져온다.
    • 가져올 코드는 Default Export여야 함
  • /* webpackPrefetch: true */ 웹팩 매직 키워드를 통해 해당 페이지 소스코드를 비동기적으로 미리 가져옴.
  • 사용자가 화면 사용 전에 코드 다운로드 기다릴 필요 없음.
import * as React from 'react'
import {useAuth} from './context/auth-context'
import {FullPageSpinner} from './components/lib'

const AuthenticatedApp = React.lazy(() =>
  import(/* webpackPrefetch: true */ './authenticated-app'),
)
const UnauthenticatedApp = React.lazy(() => import('./unauthenticated-app'))

function App() {
  const {user} = useAuth()
  return (
    <React.Suspense fallback={<FullPageSpinner />}>
      {user ? <AuthenticatedApp /> : <UnauthenticatedApp />}
    </React.Suspense>
  )
}

export {App}

적용 효과

앱이 작아서 별거 없어보이지만 앱이 커질수록 효과가 커짐

사용 안하는 코드의 대부분은 리액트-라우터임

정말 최적화가 중요하면 로그인 페이지에서 라우터를 안쓰게 변경

코드 스플리팅 전. 총 424kb의 코드를 받아오고 52%의 코드가 사용되지 않음
코드 스플리팅 후 393kb 받아오고 중 171kb 사용 안함

컨텍스트 메모이제이션

훅을 제공하는 쪽에서 메모이제이션을 잘 하면 사용자가 직접 React.Memo나 React.useCallback을 사용하지 않아도 됨.

컨텍스트에서  value로 제공하는 부분은 메모이제이션을 적용해줘야 함.

bookshelf/auth-context.extra-2.js at exercises/09-performance · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

  const login = React.useCallback(
    form => auth.login(form).then(user => setData(user)),
    [setData],
  )
  const register = React.useCallback(
    form => auth.register(form).then(user => setData(user)),
    [setData],
  )
  const logout = React.useCallback(() => {
    auth.logout()
    setData(null)
  }, [setData])

  const value = React.useMemo(() => ({user, login, logout, register}), [
    login,
    logout,
    register,
    user,
  ])

 

적용 효과

 

AuthProvider 메모이제이션 후

프론트엔드 모니터링 위한 React Profiler 적용

프로덕션 프로파일 적용 방법 with CRA

(기본적으로 꺼져있음)

npx react-script build --profile

당연히 해당 방법은 앱의 성능을 희생함.

페이스북의 경우 일부 유저만 샘플링해서 해당 방법을 적용한다 함.

인터랙션 API는 unstable 하니 주의.

인터랙션 API 리턴 객체는 Set임.  디스트럭처 해줌

bookshelf/profiler.extra-4.js at exercises/09-performance · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

import * as React from 'react'
import {client} from 'utils/api-client'


// 서버에 프로파일링 정보를 보내기 위한 부분
let queue = []

setInterval(sendProfileQueue, 5000)

function sendProfileQueue() {
  if (!queue.length) {
    return Promise.resolve({success: true})
  }
  const queueToSend = [...queue]
  queue = []
  return client('profile', {data: queueToSend})
} 


// 추가 정보 전송과 phase filtering을 위한 wrapper component
function Profiler({metadata, phases, ...props}) {
  function reportProfile(
    id, // the "id" prop of the Profiler tree that has just committed
    phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
    actualDuration, // time spent rendering the committed update
    baseDuration, // estimated time to render the entire subtree without memoization
    startTime, // when React began rendering this update
    commitTime, // when React committed this update
    interactions, // the Set of interactions belonging to this update
  ) {
    if (!phases || phases.includes(phase)) {
      queue.push({
        metadata, // 서버로 추가로 보내고 싶은 메타데이터 {metadata : Object}
        id,
        phase,
        actualDuration,
        baseDuration,
        startTime,
        commitTime,
        interactions: [...interactions], // interaction 사용 시
      })
    }
  }
  return <React.Profiler onRender={reportProfile} {...props} />
}

export {Profiler}
export {unstable_trace as trace, unstable_wrap as wrap} from 'scheduler/tracing'

 

컴포넌트에서 사용 방법

bookshelf/list-item-list.extra-3.js at exercises/09-performance · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

아래와 같이 Profiler로 감싸고 id, metadata 정보를 적용하면 된다.

function ListItemList({filterListItems, noListItems, noFilteredListItems}) {
  const listItems = useListItems()

  const filteredListItems = listItems.filter(filterListItems)

  if (!listItems.length) {
    return <div css={{marginTop: '1em', fontSize: '1.2em'}}>{noListItems}</div>
  }
  if (!filteredListItems.length) {
    return (
      <div css={{marginTop: '1em', fontSize: '1.2em'}}>
        {noFilteredListItems}
      </div>
    )
  }

  return (
    <Profiler
      id="List Item List"
      metadata={{listItemCount: filteredListItems.length}}
    >
      <BookListUL>
        {filteredListItems.map(listItem => (
          <li key={listItem.id} aria-label={listItem.book.title}>
            <BookRow book={listItem.book} />
          </li>
        ))}
      </BookListUL>
    </Profiler>
  )
}

 

인터랙션 트레이싱

bookshelf/status-buttons.extra-4.js at exercises/09-performance · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

해당 API는 Unstable함

당연히 상위 컴포넌트는 React.Profiler로 감싸져 있어야함. 

import {trace} from 'components/profiler'

// Some Component
  function handleClick() {
    if (isError) {
      reset()
    } else {
      trace(`Click ${label}`, performance.now(), () => {
        run(onClick())
      })
    }
  }

name에 trace 메세지가 적용(해당 이벤트가 트리거된 컴포넌트의 label)

 

 

 

 

데이터 update정보 트레이싱

bookshelf/hooks.extra-4.js at exercises/09-performance · kentcdodds/bookshelf (github.com)

 

GitHub - kentcdodds/bookshelf: Build a ReactJS App workshop

Build a ReactJS App workshop. Contribute to kentcdodds/bookshelf development by creating an account on GitHub.

github.com

아래와 같이 update하는 부분을 감싸준다.

import {wrap} from 'components/profiler'
////// some function
      return promise.then(
        wrap(data => {
          setData(data)
          return data
        }),
        wrap(error => {
          setError(error)
          return error
        }),
      )

해당 방법을 적용하면 인터랙션 프로파일러가 열림. 커밋들을 클릭해보자
위의 노란색 버튼을 누르면 해당 인터랙션이 트리거한 리렌더링이 보인다.

 

관련내용 심화

📜 Profile a React App for Performance

 

Profile a React App for Performance

How to use the React DevTools and React's profiling build to properly profile a production app

kentcdodds.com

📜 Interaction tracing with React

 

Interaction tracing with React

Interaction tracing with React. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

참고

 

Code-Splitting – React

A JavaScript library for building user interfaces

reactjs.org

 

First Meaningful Paint

Learn about Lighthouse's First Meaningful Paint metric and how to measure and optimize it.

web.dev

 

반응형