ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Next Image load super slow
    React.js, Next.js 2023. 1. 4. 21:39

    목차

    • 문제 상황
    • 대안
    • 결과
    • 응답 헤더 정리

    문제 상황

    현재 개발 중인 우리 웹 사이트는 next/Image를 통해 image 리소스를 제공하고 있습니다. next/image의 경우 기본 <img> 태그와 달리 원본 파일 형식(. jpg,. png, …)을 차세대 형식인 .webp 로 변환해주는 기능을 담당하고 있으며 이미지 크기(px, size 둘 다) 또한 최적화해 줍니다.

     

    이번에 발생한 문제는 next/Image를 통해 렌더링 하려는 이미지 리소스(20-40 개 또는 그 이상)가 많을 때, 이미지 리소스 자체의 크기는 줄었지만, 화면에 이미지를 정상적으로 제공할 때까지의 시간이 너무 오래 걸린다는 것이었습니다.

    이 모든 상황은 캐시가 되기 이전 최초 렌더링 상황에 대한 이야기로, 캐시가 된 이후에는 계속 성능을 보장함과 동시에 매우 빠른 속도로 렌더링 합니다.

    Page A

    Page B

    next image를 통해 webp로 변환되었고, 이미지의 용량(크기) 또한 최적화되었지만, 실제로 처리하여 화면에 그릴 때까지 캐싱되지 않은 상황이라면 약 25초가 걸리는 문제가 발생했다.

    대안

    완벽한 해결 방법이라고 할 수는 없지만, 퍼포먼스 측면에서 나은 성과를 보여준 부분에 대한 정보를 공유해볼까 합니다.

    Image loading is super slow with next/image · Discussion #21294 · vercel/next.js

     

    Image loading is super slow with next/image · Discussion #21294 · vercel/next.js

    I am using next/image as described <Image alt='alt ' src={getImage()} layout='fill' objectFit='cover' objectPosition='right bottom' /> but its superslow while ...

    github.com

    1. priority 프로퍼티 추가하기

    기본적으로 우선 렌더링을 하고 싶은 넥스트 이미지에 priority 속성을 넣어주고 이미지 리소스들 중 우선순위를 가질 수 있도록 만듭니다

    <Image
      alt="이미지 설명"
      layout={'fill'}
      objectFit={'cover'}
      src={'이미지 경로'}
    	priority
    />
    

    2. sharp library 사용하기

    next/image 컴포넌트의 경우 초기로더를 위해 squoosh를 사용한다고 명시했습니다.

     

    sharp-missing-in-production | Next.js

    Sharp Missing In Production The next/image component's default loader uses squoosh because it is quick to install and suitable for a development environment. For a production environment using next start, it is strongly recommended you install sharp. You a

    nextjs.org

    • next start 명령어를 사용하는 프로덕션 환경에서는 sharp를 사용하는 것을 적극 권장하고 있습니다
    yarn add sharp
    

    3. next config, output: “standalone” 명령어 사용하기

    • 서버리스 환경(AWS 람다 또는 구글 클라우드 등)에서 next 프레임워크의 production을 제공하는 경우, output: “standalone”을 설정할 수 있습니다
    • next start 명령어를 사용하는 프로덕션 환경에서는 output: “standalone”을 설정할 것을 권장하고 있습니다
    • 저희는 도커 컨테이너를 사용하고 있기 때문에 해당 설정에는 적합하지 않아 건너뛰었습니다
    // next.config.js
    
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      reactStrictMode: false,
      images: {
        domains: [],
        minimumCacheTTL: 31536000,
        formats: ['image/webp'],
      },
      output: 'standalone',
    };
    
    module.exports = nextConfig;
    

    👇🏼 why should we use output: "standalone" in next config production environment?

    In a Next.js application, the output: "standalone" configuration option specifies that the application should be built as a standalone serverless function, rather than as a traditional server-side rendered application. This means that the application will be deployed to a serverless environment, such as AWS Lambda or Google Cloud Functions, and will be executed as a single function in response to HTTP requests.

    There are several benefits to using the output: "standalone" option in a production environment:

    1. Serverless functions are typically more cost-effective than traditional servers, as you only pay for the execution of the function and not for the infrastructure.
    2. Serverless functions are also more scalable, as the infrastructure can automatically scale to meet demand.

    It's worth noting that the output: "standalone" option is not appropriate for all applications. If your application requires a more traditional server-side rendering setup, you may want to consider using the output: "server" option instead.

    {
    	output: 'standalone'
    }
    

    명령어는 응용 프로그램이 기존의 서버 측 렌더링 응용 프로그램이 아닌, 독립 실행형 서버리스 함수로 빌드되어야 함을 지정합니다. 이는 애플리케이션이 AWS 람다나 구글 클라우드 기능과 같은 서버가 없는 환경에 배포되고 HTTP 요청에 응답하여 단일 기능으로 실행된다는 것을 의미합니다.

    1. 서버리스 기능은 일반적으로 인프라가 아닌 기능 실행 비용만 지불하기 때문에 기존 서버보다 비용이 효율적입니다
    2. 서버리스 기능은 인프라가 수요에 맞게 자동으로 확장될 수 있기 때문에 더욱 확장 가능합니다

    해당 옵션이 모든 애플리케이션에 적합하지는 않습니다. 응용프로그램에 더 전통적인 서버 측 렌더링 설정이 필요한 경우, 대신 output: "server" 옵션을 사용하는 것을 고려할 수 있습니다.

    4. next.config에 minimumCacheTTL 프로퍼티 설정하기

    next/image | Next.js

     

    next/image | Next.js

    Enable Image Optimization with the built-in Image component.

    nextjs.org

    Images are optimized dynamically upon request and stored in the <distDir>/cache/images directory.

    The optimized image file will be served for subsequent requests until the expiration is reached.

    When a request is made that matches a cached but expired file, the expired image is served stale immediately.

    Then the image is optimized again in the background (also called revalidation) and saved to the cache with the new expiration date.

     

    이미지는 요청 시 동적으로 최적화되며 "<distDir>/cache/images" 디렉터리에 저장됩니다.

    최적화된 이미지 파일은 만료에 도달할 때까지 후속 요청에 제공됩니다.

    캐시 되었지만 만료된 파일과 일치하는 요청이 수행되면 만료된 이미지는 즉시 오래된 상태로 제공됩니다.

    그런 다음 이미지가 백그라운드에서 다시 최적화되고 새 만료 날짜와 함께 캐시에 저장됩니다.

     

    You can configure the Time to Live (TTL) in seconds for cached optimized images. In many cases, it's better to use a Static Image Import which will automatically hash the file contents and cache the image forever with a Cache-Control header of immutable.

     

    최적화된 캐시 된 이미지에 대해 TTL(Time to Live)을 초 단위로 구성할 수 있습니다. 대부분의 경우 파일 내용을 자동으로 해시하고 변경 불가능한 Cache-Control의 헤더로 이미지를 영원히 캐시 하는 정적 이미지 가져오기를 사용하는 것이 좋습니다.

    module.exports = {
      images: {
        minimumCacheTTL: 60,
      },
    }
    

    우리는 캐시 기간을 늘리기 위해서 minimumCacheTTL에 기본 최대 TTL 값인 ‘31536000’ 을 설정했습니다.

    /** @type {import('next').NextConfig} */
    const nextConfig = {
      reactStrictMode: false,
      images: {
        domains: ['CDN 주소'],
        minimumCacheTTL: 31536000,
        formats: ['image/webp'],
      },
    };
    
    module.exports = nextConfig;
    

    5. lambda를 통한 custom loader 사용하기

    Next image에는 loader 라는 프로퍼티가 있습니다. 로더는 다음과 같은 기능을 제공합니다.

    Loaders

    Note that in the example earlier, a partial URL "/me.png" is provided for a remote image. This is possible because of the loader architecture.

    A loader is a function that generates the URLs for your image. It modifies the provided src, and generates multiple URLs to request the image at different sizes. These multiple URLs are used in the automatic srcset generation, so that visitors to your site will be served an image that is the right size for their viewport.

    The default loader for Next.js applications uses the built-in Image Optimization API, which optimizes images from anywhere on the web, and then serves them directly from the Next.js web server. If you would like to serve your images directly from a CDN or image server, you can write your own loader function with a few lines of JavaScript.

    You can define a loader per-image with the [loader prop](https://nextjs.org/docs/api-reference/next/image#loader), or at the application level with the [loaderFile configuration](https://nextjs.org/docs/api-reference/next/image#loader-configuration).

     

    원래 next/image의 구조에서는 me.png 라는 원본 이미지가 있을 경우 next server에 의해 이미지가 최적화되어 .webp 버전과 최적화된 사이즈로 제공됩니다. 하지만, 로더를 사용할 경우 우리가 설정한 이미지의 형식을 prop으로 넘겨 그대로 전달할 수 있습니다.

     

    로더는 이미지의 URL을 생성하는 기능입니다. 제공된 'src'를 수정하고 여러 URL을 생성하여 다른 크기의 이미지를 요청할 수 있습니다.

     

    이때 우리는 lambda를 통해 custom loader를 구현하여 next/image의 이점을 사용하면서 커스텀하게 이미지를 제공할 수 있습니다.

     

    람다를 통해 custom loader를 사용할 경우 다음의 과정으로 변하게 됩니다.

    출처- AWS Lambda

    • Step 1: The requested image URI is manipulated in the viewer-facing Lambda@Edge function to serve appropriate dimension and format. This happens before the request hits the cache. Refer code snippet 1 below.
    • Step 2: CloudFront fetches the object from origin.
    • Step 3: If the required image is already present in the bucket or is generated and stored (via step 5), CloudFront returns the object to viewer. At this stage, the image is cached.
    • Step 4: The object from cache is returned to user.
    • Step 5: Resize operation is invoked only when an image is not present in origin. A network call is made to the S3 bucket (origin) to fetch the source image and resized. The generated image is persisted back to the bucket before sending to CloudFront.

    커스텀 로더를 통해 제공할 이미지를 next-server가 아닌 람다로 보내고, 람다 내에서 전처리과정을 거친 이미지를 src 에 보여주게 됩니다. 람다의 전처리과정(형식 변환 및 크기 변환)이 없다면, CDN에 저장된 이미지가 원본 형식(size, px) 그대로 이미지를 제공할 것입니다. 따라서 람다를 통해 px,size, 형식을 최적화할 수 있습니다. 결과적으로 람다 함수를 구성할 때 next/image 의 제공 라이브러리인 squoosh 대신 sharp를 쓰는 것은 동일하지만, 이 처리 과정을 웹 프론트 코드 내에서 진행하는지와 람다 함수를 통해 제공하는지에 차이가 있을 것입니다.

     

    람다에 대한 내용은 따로 담지 않겠습니다.

    결과

    next/image의 로딩이 느린 방법을 개선하기 위해서 제가 찾은 방법은 2가지가 있습니다.

    • 프론트 코드 내에서 적용할 수 있는 방법 (1,2,4)
    • 커스텀 로더와 람다를 이용하는 방법 (2, 5)

    이 글에는 프론트 코드 내에서 적용할 수 있는 방법(1,2,4)을 적용하였습니다.

    page A

    page B

    기존 응답 헤더

    x-nextjs-cache : STALE, HIT

    max-age=60

    변경된 응답 헤더

    x-nextjs-cache : HIT

    max-age=31536000

    캐시 관련 추가 지식

    x-nextjs-cache

    response header. The possible values are the following:

    • MISS - the path is not in the cache (occurs at most once, on the first visit)
    • STALE - the path is in the cache but exceeded the revalidate time so it will be updated in the background
    • HIT - the path is in the cache and has not exceeded the revalidate time

    Cache-Control

    • no-cache - 캐시 관리자에게 캐싱을 허용하나, 사용 전 필수로 Server에 유효성 체크를 해야 합니다.
    • no-store - 캐싱을 금지합니다.
    • max-age=3600 - 초 단위로 캐시가 유효한 시간을 제공합니다. (3600초)
    • must-revalidate - 캐시 관리자에게 내부 유효성 체크를 허용하지 않으며, 항상 서버에 유효성 체크를 하도록 합니다.

    댓글

Designed by Tistory.