06-Nuxt SSR 최적화 팁

업데이트: Link

Nuxt SSR 최적화 팁

Part 6 of 6 in our Vue.js Performance series.
Written by Filip Rakowski

오늘의 Vue 성능 시리즈 부분에서는 Vue.js 생태계에서 가장 흥미로운 프레임워크인 Nuxt에 초점을 맞출 것입니다. 특히 SSR(Server-Side Rendering) 메커니즘이 성능에 미치는 영향과 이를 최적화하기 위해 수행할 수 있는 작업에 중점을 둘 것입니다. 물론 이 시리즈의 모든 이전 팁은 Nuxt에서 여전히 실행 가능합니다!

서버사이드 렌더링은 어떻게 작동합니까?

SSR을 최적화하는 방법을 배우려면 작동 방식과 클라이언트 측 렌더링과의 차이점을 이해하는 것이 중요합니다.

클라이언트 측에서 렌더링된 SPA(Single Page Application)에 들어가면 먼저 index.html의 콘텐츠가 있는 빈 화면이 표시됩니다(일반적으로 body 안에 있는 <div id="app"></div>). 그런 다음 JavaScript가 실행을 시작하고 UI를 동적이고 점진적으로 만듭니다. 완전히 로드될 때까지 화면의 콘텐츠가 어떻게 변경되는지 확인할 수 있습니다.

먼저 우리는 일반적으로 머리글, 바닥글 및 페이지 콘텐츠의 일부를 봅니다. 콘텐츠의 첫 번째 비트가 화면에 나타나는 데 필요한 시간을 설명하는 메트릭을 First Contentful Paint - FCP라고 합니다. 앱이 시각적으로 완전하고 완전히 작동할 때까지 UI의 다른 부분이 표시됩니다. 앱이 완전히 대화형이 되는 데 필요한 시간을 측정하는 메트릭을 대화형 시간 - TTI라고 합니다.

참고 사항: 웹 성능을 측정하는 측정항목에 대해 자세히 알아보려면 Artem Denysov의 이 훌륭한 기사를 확인하세요. )

서버 측 렌더링 앱에서는 이러한 진행 상황을 볼 수 없습니다. 로드하는 방법은 다음과 같습니다.

  • 먼저 JavaScript 코드가 서버에서 실행되고 Vue는 애플리케이션의 전체 마크업이 포함된 정적 HTML 파일을 생성합니다.
  • 이 정적 HTML 파일은 브라우저로 전송됩니다. 사용자가 수신하는 데 필요한 시간을 설명하는 메트릭을 TTFB(Time to First Byte)라고 합니다.
  • 일단 다운로드되면 사용자는 거의 즉시 전체 페이지를 볼 수 있습니다(하지만 아직 대화형은 아닙니다!)
  • JavaScript는 클라이언트 측에서 실행되고 정적 HTML의 제어를 인수하여 대화형으로 만듭니다. 이 과정을 수화(hydration)라고 합니다. Vue 상호 작용(water)과 함께 정적(dehydrated) HTML을 제공하는 것으로 생각할 수 있습니다.
  • 하이드레이티드(hydrated) 앱은 인터랙티브(TTI)가 됩니다.

서버 측 렌더링 프로세스는 우리가 웹사이트에 직접 들어갈 때만(또는 새로 고칠 때) 발생한다는 점을 언급하는 것이 중요합니다. 수화되면 앱은 일반 클라이언트 측 렌더링 SPA처럼 작동합니다.

어떤 지표에 초점을 맞춰야 할까요?

서버 측 렌더링이 작동하는 방식을 알면 이 분야에서 뛰어난 성능을 달성하기 위해 애플리케이션을 최적화해야 하는 두 가지 주요 메트릭이 있다는 결론을 내릴 수 있습니다.

  • Time to First Byte(TTFB) - . 즉, 렌더링된 정적 HTML이 사용자의 브라우저에 도착할 때까지의 시간입니다. 점진적으로 로드되는 클라이언트 측 렌더링 앱과 달리 사용자는 전체 페이지가 다운로드될 때까지 아무것도 볼 수 없기 때문에 이 측정항목을 가능한 한 낮게 유지하는 것이 중요합니다(평균 네트워크에서 이상적으로는 약 1초).
  • Time to Interactive(TTI) - 서버 측에서 렌더링된 페이지가 사용자에게 빠르게 전달될 수 있다고 해도 가능한 한 빨리 대화형으로 만드는 것도 마찬가지로 중요합니다. 그렇지 않으면 사용자가 의도한 대로 작동하지 않는 동적 요소에 좌절할 수 있습니다.

참고 사항: 인앱 탐색을 수행하는 동안 애플리케이션은 일반 SPA처럼 작동하므로 다른 자산의 번들 크기와 런타임 성능을 계속 처리해야 합니다!

TTFB 최적화

총 시간을 첫 번째 바이트까지의 총 시간을 2단계로 나눌 수 있습니다.

  • 서버 측 코드가 실행되고 정적 HTML 파일이 생성되는 실행 단계
  • 생성된 HTML 파일이 사용자 브라우저에 다운로드되는 “다운로드” 단계

이전 기사에서 실행 단계를 최적화하는 방법을 이미 알고 있습니다. 알려진 대부분의 클라이언트 측 최적화 기술은 이 지표에 긍정적인 영향을 미칩니다.

까다로운 부분은 다운로드 단계입니다. 이는 출력된 HTML 파일의 크기와 엄격하게 연관되며 이는 쉽게 제어할 수 없습니다. 그 이유를 이해하려면 Nuxt가 서버 측에서 가져온 데이터를 클라이언트 측으로 전달하는 방법과 서버 측에서 생성된 HTML의 크기에 미치는 영향을 배워야 합니다.

서버 측 데이터 전달

동일한 코드가 서버 측과 클라이언트 측에서 실행된다고 썼습니다. 또한 모든 비동기식 호출이 양쪽에서 이루어짐을 의미하지만 이는 완전히 사실이 아닙니다. 코드를 있는 그대로 작성하면 이런 일이 발생하지만 시간과 대역폭을 크게 낭비하게 됩니다. 정적 HTML을 생성하기 위해 이미 서버 측에서 데이터를 가져오는 중이라면 클라이언트 측에서 이 작업을 다시 수행하는 요점은 무엇입니까?

Nuxt core 팀은 이 문제를 매우 잘 알고 있으며 이것이 서버 측 데이터를 클라이언트 측으로 전달하는 데 사용되는 fetchasyncData를 도입한 이유입니다.

Nuxt 문서에서 이 예를 살펴보세요.


<template>
  <div>
    <h1>Blog posts</h1>
    <p v-if="$fetchState.pending">Fetching posts...</p>
    <p v-else-if="$fetchState.error">
      Error while fetching posts: {{ $fetchState.error.message }}
    </p>
    <ul v-else>
      <li v-for="post of posts" :key="post.id">
        <n-link :to="`/posts/${post.id}`">{{ post.title }}</n-link>
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        posts: []
      }
    },
    async fetch() {
      this.posts = await this.$http.$get(
        'https://jsonplaceholder.typicode.com/posts'
      )
    }
  }
</script>

URL에서 직접 페이지를 입력하면(SSR을 사용함) 네트워크 탭에서 https://jsonplaceholder.typicode.com/posts에 대한 추가 요청을 볼 수 없습니다. 이는 서버의 fetch에서 가져온 모든 것을 추가 네트워크 호출 없이 클라이언트 측에서 사용할 수 있기 때문입니다.

좋아요, 우리는 무슨 일이 일어나는지 알고 있지만 어떻게 일어나는지는 모릅니다. 이 데이터는 클라이언트에 마술처럼 나타나지 않습니다. 전달할 수 있는 방법이 있어야 합니다!

우리가 서버에서 얻는 유일한 것은 이 거대한 index.html 파일이므로 데이터가 거기에 있어야 합니다! 이 파일의 소스를 확인하면 맨 아래에 NUXT 객체를 window에 추가하는 <script> 태그를 알 수 있습니다. 이것은 서버 측 데이터가 있는 곳입니다! Nuxt는 자동으로 새 상태를 선택합니다.

<script>
window.__NUXT__={
	layout:"default",
	data:[
		{},
		{
			posts:[
				{
					userId:1,
					id:1,
					title:"sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
					body:"quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
				},
				{
					userId:1,
					id:2,
					title:"qui est esse",
					body:"est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
				},
				{
					userId:1,
					id:3,
					title:"ea molestias quasi exercitationem repellat qui ipsa sit aut",
					body:"et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut [...]"
				}
			]
		}
	],
	error:null,serverRendered:!0
}
</script>

이 강력한 도구를 사용하면 클라이언트 측에서 콘텐츠를 가져오는 시간을 절약할 수 있지만 index.html의 크기를 크게 늘릴 수도 있습니다. 전달하기로 결정한 데이터가 많을수록 데이터가 커지고 사용자가 모든 콘텐츠를 보기 위해 기다려야 하는 시간이 길어집니다. 이것이 우리가 클라이언트 측에 보내는 데이터에 매우 주의해야 하는 이유입니다!

말 그대로 해당 분야에서 성능 문제를 피하기 위해 우리가 할 수 있는 한 가지가 있습니다 - 우리가 할 필요가 없는 데이터를 보내지 마십시오. 뻔한 얘기 같지만 정확히 어떤 데이터일까요?

탐색 또는 메인 페이지 콘텐츠와 같이 웹사이트의 SEO에 중요한 부분을 표시하는 데 필요한 모든 것은 항상 프론트엔드 측에 전달되어야 합니다.

로그인한 사용자에게만 제공되는 데이터 또는 기타 개인화된 콘텐츠(예: 장바구니) 및 팝업/오프스크린 사이드바 콘텐츠와 같이 크롤러에게 중요하지 않은 콘텐츠는 전달할 필요가 없습니다.

또한 클라이언트에 보내야 하는 개체를 가능한 한 작게 만들어야 합니다.

  • GraphQL을 사용하는 경우 쿼리의 필드를 항상 실제로 사용되는 필드로 제한해야 합니다.
  • GraphQL을 사용하지 않는 경우 구성 요소 상태에 저장하는 객체에서 필드를 제거하여 필요한 필드로 보내는 필드를 제한하십시오. 예를 들어 아래 코드에서는 postsidtitle 속성만 필요하므로 다른 속성을 제거할 수 있습니다.
<script>
  export default {
    data() {
      return {
        posts: []
      }
    },
    async fetch() {
      this.posts = await this.$http
        .$get('https://jsonplaceholder.typicode.com/posts')
        .then(posts =>
            posts.map(posts => ({
              title: posts.title,
              id: posts.id
            }))
          );
    }
</script>

이제 TTI를 최적화하는 방법을 알았으므로 앱을 더 빠르게 대화형으로 만들기 위해 무엇을 할 수 있는지 알아보겠습니다.

중요한 참고 사항: 전달되는 데이터를 제한하는 것 또한 절충점이라는 점을 명심하십시오! 더 나은 성능을 얻고 있지만 JS 실행이 실패하면(그리고 그에 따라 수화 되면) 일부 중요한 데이터를 놓치고 앱을 사용할 수 없게 될 수 있습니다. JavaScript가 실패하더라도 앱이 작동하도록 하려면 사용자 탐색에 필요한 모든 것을 전달하고 있는지 확인해야 합니다. 또한 JavaScript 없이는 작동하지 않기 때문에 오프스크린 사이드바 및 팝업과 같은 동적 UI 요소를 제거해야 합니다.

Time to Interactive 최적화

앱에 있는 JS 코드의 전체 양과 수화되어야 하는 구성 요소의 수는 대화형 시간 측정에 영향을 미치는 두 가지 핵심 요소입니다. 우리는 이미 이 시리즈의 이전 부분에서 중요한 경로에서 JavaScript의 양을 최소화할 수 있는 효과적인 코드 분할 기술을 알고 있지만 수화된 구성 요소의 양을 최소화하기 위해 할 수 있는 일이 있습니까?

다행히 Markus Oberlehner가 만든 놀라운 vue-lazy-hydration 라이브러리 덕분에 우리는 많은 일을 할 수 있습니다!

라이브러리 README에서 읽을 수 있듯이:

vue-lazy-hydration은 렌더링되지 않은 Vue.js 구성 요소로 서버 측에서 렌더링된 Vue.js 애플리케이션의 예상 입력 대기 시간 및 대화형 시간을 개선합니다.”

바로 우리가 찾고 있는 것입니다! README는 우리가 기대할 수 있는 결과를 보여주는 예를 제공합니다.

다음은 지연 수화 없는 테스트 프로젝트의 결과입니다.

그리고 이것들은 - 게으른 수화 작용이 적용된 경우:

위의 예에서 TTI가 vue-lazy-hydrate으로 25% 더 작음을 알 수 있습니다(물론 결과는 완전히 다를 수 있음)! 이 라이브러리를 사용하는 것이 매우 쉽기 때문에 빠르고 중요한 성능 향상을 달성할 수 있는 훌륭한 도구라는 것을 곧 알게 될 것입니다.

라이브러리를 설치하려면 npm/yarn 레지스트리를 통해 프로젝트에 라이브러리를 추가하기만 하면 됩니다.

    npm install vue-lazy-hydration --save

이제 다른 구성 요소를 감싸고 수화를 지연시키는 데 사용할 수 있는 LazyHydrate 구성 요소에 액세스할 수 있습니다.

예를 들어 다음은 화면에 표시될 때까지 component 수화를 잠시 지연하는 방법입니다.

<template>
  <div>
    <LazyHydrate when-visible>
      <AdSlider/>
    </LazyHydrate>
  </div>
</template>

<script>
import LazyHydrate from 'vue-lazy-hydration';

export default {
  components: {
    LazyHydrate,
    AdSlider: () => import('./AdSlider.vue'),
  },
  // ...
};
</script>

팁: 라이브러리를 사용하면 다른 조건(예: on-interaction)에서도 구성요소를 수화할 수 있습니다. README에서 사용 가능한 옵션을 확인할 수 있습니다.

그리고 그게 끝입니다! 이 라이브러리의 사용법은 엄청나게 간단합니다. 이제 언제 사용할 수 있는지 봅시다.

구성 요소를 세 그룹으로 나눌 수 있습니다.

Components 즉시 수화 되어야 하는

일반적으로 화면에서 즉시 볼 수 있는 구성요소입니다(폴더 위라고도 함). 우리는 그들에 대해 많은 것을 할 수 없습니다. 그들은 바로 수화 되어야 합니다.

Components 나중에 수화될 수 있는

대부분의 경우 이러한 요소는 화면 밖에 있는 구성요소 또는 접을 수 있는 구성요소 에 포함되어 있습니다.

나타날 때 수화 되는 것이 대부분의 경우 올바른 선택 전략입니다.

<LazyHydrate when-visible>
  <LazyHydratedComponent/>
</LazyHydrate>

Components 전혀 수화될 필요 없는

예! 그러한 구성 요소가 있습니다. 우리는 종종 일부 텍스트만 표시하지만 어떤 식으로든 상호 작용하지 않는 구성 요소를 가지고 있습니다. 이러한 구성 요소는 일단 서버에서 렌더링되면 정적으로 유지될 수 있으며 전혀 수화될 필요가 없습니다.

우리는 LazyHydrate 구성요소의 ssr-only 소품을 사용하여 이를 달성할 수 있습니다.

<LazyHydrate ssr-only>
  <ArticleContent />
</LazyHydrate>

요약

Nuxt 성능 최적화는 다른 Vuejs 애플리케이션을 최적화하는 것과 크게 다르지 않습니다. 클라이언트 측 부분은 일반 Vue 앱으로 작동하지만 초기 방문 시 페이지가 서버 측에서 렌더링되므로 앱이 로드될 때까지 사용자에게 빈 화면이 표시됩니다. 이것이 이 초기 콘텐츠를 가능한 한 빨리 제공하는 것이 중요한 이유입니다. 잠재적인 성능 병목 현상의 영역을 알고 있다면 그것은 누워서 떡먹기 일 것입니다!

댓글남기기