렌더링 최적화

렌더링 최적화

브라우저가 웹 페이지를 렌더링하려면 이미지, 글꼴, 스타일시트 같은 추가 자원이 필요합니다. 이 자원들은 대부분 원격 서버에 있어서 HTTP 요청으로 불러와야 합니다.

이런 추가 요청은 렌더링 성능에 큰 영향을 줍니다. 웹 페이지가 빠르게 로드되려면 이러한 HTTP 요청을 효율적으로 제어해야 합니다.

이번 포스팅에서는 브라우저의 렌더링 과정을 이해하고, 외부 리소스의 로드 시점을 조절해서 웹 성능을 최적화하는 기법들을 살펴보겠습니다.

렌더링 최적화
렌더링 최적화


1. 렌더링 과정

사용자가 주소창에 URL을 입력하면 브라우저는 먼저 DNS(도메인 네임 서버)에서 IP 주소를 얻습니다. 이 IP 주소로 서버와 연결한 뒤 HTTP 통신을 시작합니다.

브라우저는 URL 경로에 해당하는 문서를 요청하고, 서버는 준비된 문서를 응답합니다.

문서를 받은 브라우저는 이를 화면에 그릴 수 있는 형태로 변환해야 합니다. 이 과정을 Critical Rendering Path(CRP)라고 합니다.

CRP는 다음 5단계로 진행됩니다.

먼저 파싱 단계입니다. HTML 문서를 파싱해서 DOM을 만들고, CSS 코드는 CSSOM으로 만듭니다.

다음은 렌더 트리 생성입니다. DOM과 CSSOM을 합쳐서 렌더 트리를 만듭니다.

레이아웃 단계에서는 각 요소의 크기와 위치를 계산합니다.

마지막으로 페인팅 단계에서 계산된 레이아웃을 기반으로 실제 픽셀로 화면에 그립니다.

Critical Rendering Path(CRP) 과정:

HTML 파싱 ──────→ DOM 생성

CSS 파싱 ───────→ CSSOM 생성

                렌더 트리 생성

                  레이아웃
                  (Reflow)

                  페인팅
                 (Repaint)

파싱 단계를 자세히 살펴보겠습니다.

HTML은 개발자가 이해할 수 있는 마크업 코드입니다. 브라우저가 이를 화면에 그리려면 자신이 이해할 수 있는 형태로 변환해야 합니다.

파싱은 코드를 한 줄씩 읽으면서 의미 있는 단위로 분석하는 과정입니다. 그런데 파싱 중에 특정 코드를 만나면 브라우저는 파싱을 멈추고 그 코드를 먼저 처리합니다.

예를 들면 아래와 같은 자바스크립트를 로딩하는 코드입니다.

<script src="script.js"></script>html

HTML 문서에서 이런 스크립트 태그를 만나면 브라우저는 파싱을 멈춥니다. 자바스크립트를 다운로드하기 위한 HTTP 요청을 보내고, 응답받은 파일을 실행합니다.

자바스크립트 다운로드와 실행이 완료되면 중단했던 지점으로 돌아가 파싱을 재개합니다.

이처럼 스크립트 태그는 파싱을 중단시켜 렌더링 시간을 늘립니다. 자바스크립트 파일이 많거나 무거울수록 페이지 로드 속도가 느려집니다.

핵심 정리: 브라우저의 렌더링 과정은 파싱 → DOM/CSSOM → 렌더 트리 → 레이아웃 → 페인팅의 5단계를 거치며, <script> 태그를 만나면 파싱을 중단하고 자바스크립트를 다운로드/실행한 후 재개합니다.


2. script 태그의 렌더링 영향도

아래는 public 폴더의 정적 파일들을 제공하는 간단한 서버 코드입니다.

const http = require('http');
const path = require('path');
const static = require('../shared/serve-static');

const handler = (req, res) => {
  static(path.join(__dirname, 'public'))(req, res);
};

const server = http.createServer(handler);
const port = process.env.PORT || 3000;
server.listen(port, () => console.log(`server is running ::${port}`));js

index.html 코드는 아래와 같습니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <style>
      img {
        width: 50%;
      }
    </style>
  </head>
  <body></body>
</html>html

큰 파일과 작은 파일을 순서대로 로딩하는 스크립트를 추가해보겠습니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <style>
      img {
        width: 50%;
      }
    </style>
  </head>

  <body>
    <script src="script-big.js"></script>
    <script src="script-small.js"></script>
  </body>
</html>html

서버에서 제공할 자바스크립트 파일을 만들어보겠습니다.

script-big.js는 콘솔에 파일명을 출력합니다.

console.log('script-big.js');js

script-small.js도 마찬가지입니다.

console.log('script-small.js');js

두 스크립트는 각자 파일명을 콘솔에 출력합니다. 파일 크기를 시뮬레이션하기 위해 응답 시간을 다르게 설정해보겠습니다.

serve-static.js 파일을 보면 다음과 같습니다.

const fs = require('fs');
const path = require('path');

const serveStatic = (root) => {
  return (req, res) => {
    const filepath = path.join(root, req.url === '/' ? '/index.html' : req.url);

    fs.readFile(filepath, (err, data) => {
      if (err) {
        if (err.code === 'ENOENT') {
          res.statusCode = 404;
          res.write('Not Found\n');
          res.end();
          return;
        }

        res.statusCode = 500;
        res.write('Internal Server Error\n');
        res.end();
        return;
      }

      const ext = path.extname(filepath).toLowerCase();
      let contentType = 'text/html';
      switch (ext) {
        case '.html':
          contentType = 'text/html';
          break;
        case '.js':
          contentType = 'text/javascript';
          break;
        case '.css':
          contentType = 'text/css';
          break;
        case '.png':
          contentType = 'image/png';
          break;
        case '.json':
          contentType = 'application/json';
          break;
        case '.otf':
          contentType = 'font/otf';
          break;
        default:
          contentType = 'application/octet-stream';
      }
      res.setHeader('Content-Type', contentType);

      // 인터페이스 추가
      if (res.delayMs) {
        setTimeout(() => {
          res.write(data);
          res.end();
        }, res.delayMs);

        return;
      }

      res.write(data);
      res.end();
    });
  };
};

module.exports = serveStatic;js

이 핸들러는 public 폴더의 파일들을 읽어서 응답합니다. res.delayMs 속성으로 응답 지연 시간을 설정할 수 있게 인터페이스를 추가했습니다.

server.js에서 파일명에 따라 지연 시간을 설정해보겠습니다.

const http = require('http');
const path = require('path');
const static = require('../shared/serve-static');

const handler = (req, res) => {
  // filename 가져오기
  const filename = path.basename(req.url);

  if (filename === 'script-big.js') {
    // 3초 후 응답
    res.delayMs = 3_000;
  }

  if (filename === 'script-small.js') {
    // 1초 후 응답
    res.delayMs = 1_000;
  }

  static(path.join(__dirname, 'public'))(req, res);
};

const server = http.createServer(handler);
const port = process.env.PORT || 3000;
server.listen(port, () => console.log(`server is running ::${port}`));js

script-big.js는 3초, script-small.js는 1초 후에 응답하도록 설정했습니다.

Firefox에서 테스트하려면 about:config에서 network.http.max-persistent-connections-per-server를 6에서 0으로 변경해야 합니다.

이 설정은 서버당 동시 연결 수를 제한하는 옵션으로, 0으로 설정하면 스크립트가 순차적으로 로드되는 것을 확인할 수 있습니다.

network.http.max-persistent-connections-per-server을 6 -> 0으로 변경
network.http.max-persistent-connections-per-server을 6 -> 0으로 변경

그리고 로컬호스트 3000번에 접속을 해보겠습니다.

로컬호스트 3000번에 접속
로컬호스트 3000번에 접속

script-big.js가 로딩되고 이어서 script-small.js가 로딩됐습니다.

브라우저가 HTML을 파싱하다가 첫 번째 스크립트 태그를 만나면 파싱을 멈춥니다.

script-big.js를 다운로드하기 위해 네트워크 요청을 보내고, 3초 후 파일을 받아 실행합니다.

다시 파싱을 재개하다가 두 번째 스크립트 태그를 만나면 또 파싱을 멈추고 script-small.js를 요청합니다.

script-big.js, script-small.js를 가져오기 위한 네트워크 요청을 만든다
script-big.js, script-small.js를 가져오기 위한 네트워크 요청을 만든다

스크립트 로딩을 완료한 후 파싱을 재개해서 DOM을 완성합니다. 두 스크립트를 순차적으로 처리하는 데 약 4초가 걸렸습니다.

정확한 시간을 측정하기 위해 Performance API를 사용해보겠습니다.

performance.mark로 측정 시작 시점을 표시합니다.

<body>
  <!-- 측정 시각을 표기할 수 있음 -->
  <script>
    performance.mark('script-big-start');
  </script>
  <script src="script-big.js"></script>
  <script>
    performance.mark('script-small-start');
  </script>
  <script src="script-small.js"></script>
</body>html

performance.mark로 종료 시점을 표시하고, performance.measure로 실제 시간을 측정합니다.

console.log('script-big.js');

// 측정 종료 시각 표기
performance.mark('script-big-end');
performance.measure(
  'script-big execution time',
  'script-big-start',
  'script-big-end'
);js
console.log('script-small.js');

// 측정 종료 시각 표기
performance.mark('script-small-end');
performance.measure(
  'script-small execution time',
  'script-small-start',
  'script-small-end'
);js

브라우저는 모든 리소스 로딩이 완료되면 load 이벤트를 발생시킵니다. 이 이벤트에서 측정 결과를 확인해보겠습니다.

<script>
  // DOM이 완성되면 발생하는 이벤트
  window.addEventListener('DOMContentLoaded', () => {
    console.log('DOMContentLoaded');
  });

  // 모든 리소스 로딩이 완료되면 발생하는 이벤트
  window.addEventListener('load', () => {
    console.log('load');
    // 측정한 성능 데이터 가져오기
    const entries = performance.getEntriesByType('measure');
    entries.forEach((entry) => {
      const result = `${entry.name} : ${entry.startTime}, ${entry.duration} ms`;
      console.log(result);
    });
  });
</script>html

performance.mark를 통해 측정 시각을 표기할 수 있음
performance.mark를 통해 측정 시각을 표기할 수 있음

결과를 보면 script-big은 45ms에 시작해서 3초, script-small은 3초쯤 시작해서 1초가 걸렸습니다.

총 4초가 소요됐습니다. 스크립트가 없었다면 1초 안에 렌더링이 완료됐을 것입니다. 스크립트 태그가 파싱을 중단시켜 렌더링 성능에 영향을 준 것입니다.


3. Async 스크립트

렌더링 성능을 개선하려면 파싱이 중단되지 않아야 합니다.

파싱과 HTTP 요청을 하나의 메인 스레드가 처리하기 때문에 파싱이 멈춥니다. 하지만 브라우저에는 I/O 스레드라는 별도 스레드가 있어서 네트워크 작업을 처리할 수 있습니다.

async 속성을 사용하면 I/O 스레드가 스크립트를 다운로드하는 동안 메인 스레드는 파싱을 계속할 수 있습니다.

<body>
  <script>
    performance.mark('script-big-start');
  </script>
  <script src="script-big.js" async></script>
  <script>
    performance.mark('script-small-start');
  </script>
  <script src="script-small.js" async></script>
</body>html

브라우저는 script-big과 script-small을 동시에 다운로드하면서 파싱을 계속 진행합니다.

브라우저 설정을 복구하고 확인해보겠습니다.

위에서 6으로 바꾼 것을 다시 0으로 변경
위에서 6으로 바꾼 것을 다시 0으로 변경

위와 다르게 async 속성 시 스크립트 파일을 동시에 다운로드 함.
위와 다르게 async 속성 시 스크립트 파일을 동시에 다운로드 함.

스크립트 파일들이 29ms에 동시에 다운로드를 시작했습니다.

메인 스레드가 DOM을 먼저 만들고, 더 가벼운 script-small.js가 먼저 다운로드 완료되어 1초쯤 실행됐습니다.

이어서 script-big도 다운로드가 완료되면 실행됩니다.

기존 1초 + 3초 = 4초 이지만, async 속성시 3초 소요
기존 1초 + 3초 = 4초 이지만, async 속성시 3초 소요

총 시간이 4초에서 3초로 줄었습니다.

async는 스크립트를 동시에 다운로드하고 완료된 순서대로 실행합니다. script-big.js를 먼저 작성했지만, script-small.js가 빨리 다운로드되어 먼저 실행됐습니다.

async는 DOM과 무관하고 서로 독립적인 스크립트에 적합합니다. 광고 스크립트나 분석 도구처럼 애플리케이션과 무관한 스크립트에 사용하면 좋습니다.

async vs defer 실행 순서 비교:

일반 스크립트:
HTML 파싱 → [중단] → 스크립트 다운로드 → 스크립트 실행 → [파싱 재개]

async 스크립트:
HTML 파싱 ────────────────────────→ DOM 생성
     ↓ (병렬)
스크립트 다운로드 → [다운로드 완료 시 즉시 실행]

defer 스크립트:
HTML 파싱 ────────────────────────→ DOM 생성 → [순서대로 실행]
     ↓ (병렬)
스크립트 다운로드 ─────────────────┘

핵심 정리: async 속성은 스크립트를 병렬로 다운로드하고 완료된 순서대로 실행합니다. DOM과 무관하고 서로 독립적인 스크립트(광고, 분석 도구 등)에 적합합니다.


4. Defer 스크립트

async는 빠르지만 실행 순서를 보장하지 않습니다.

스크립트가 서로 의존적일 때는 문제가 될 수 있습니다. script-big.js에 foo 함수를 정의해보겠습니다.

console.log('script-big.js');

// 전역 객체에 함수 등록 (다른 스크립트에서 사용 예정)
window.foo = () => console.log('foo is executed');

performance.mark('script-big-end');
performance.measure(
  'script-big execution time',
  'script-big-start',
  'script-big-end'
);js

script-small.js에서 이 foo 함수를 호출하도록 해보겠습니다.

console.log('script-small.js');

// 전역 객체에 등록된 함수 호출
foo();

performance.mark('script-small-end');
performance.measure(
  'script-small execution time',
  'script-small-start',
  'script-small-end'
);js

script-small.js는 foo 함수가 이미 정의되어 있다고 가정합니다. 실행해보겠습니다.

async 시 스크립트 크기가 작은 것을 먼저 로드하기 때문에, 에러가 발생
async 시 스크립트 크기가 작은 것을 먼저 로드하기 때문에, 에러가 발생

코드에서는 big을 먼저 작성했지만, async로 인해 small이 먼저 실행됐습니다. small이 실행될 때 foo 함수가 아직 없어서 오류가 발생했습니다.

다운로드는 동시에 하면서도 실행 순서를 보장하려면 defer 속성을 사용해야 합니다. defer는 파일을 동시에 다운로드하지만 DOM이 생성될 때까지 실행을 미룹니다.

defer를 추가해보겠습니다.

<body>
  <script>
    performance.mark('script-big-start');
  </script>
  <script src="script-big.js" defer></script>
  <script>
    performance.mark('script-small-start');
  </script>
  <script src="script-small.js" defer></script>
</body>html

다시 브라우저를 실행해보겠습니다.

defer 시 스크립트 파일을 동시에 다운로드 함. async와 다르게 코드 순서대로 실행
defer 시 스크립트 파일을 동시에 다운로드 함. async와 다르게 코드 순서대로 실행

브라우저는 두 스크립트를 40ms쯤 동시에 다운로드 시작합니다.

DOM이 만들어진 후 코드에 작성된 순서대로 big-script를 먼저, small-script를 나중에 실행합니다.

defer는 빨리 다운로드되더라도 코드 순서를 지켜서 실행합니다.

동일하게 총 3초 정도 걸리며 순서대로 실행됨을 확인할 수 있음.
동일하게 총 3초 정도 걸리며 순서대로 실행됨을 확인할 수 있음.

defer는 서로 의존적인 스크립트에 적합합니다.

webpack 같은 번들러는 chunk라고 부르는 작은 자바스크립트 파일들로 애플리케이션을 분할합니다. 이 chunk들은 의존성 때문에 순서대로 실행되어야 합니다.

그래서 webpack이 HTML에 chunk를 추가할 때 defer 속성을 사용합니다.

핵심 정리: defer 속성은 스크립트를 병렬로 다운로드하되, DOM 생성 후 코드에 정의된 순서대로 실행합니다. 서로 의존적인 스크립트나 webpack chunk 파일에 적합합니다.


5. Preload 링크

HTTP 요청이 필요한 리소스는 자바스크립트뿐만이 아닙니다. 글꼴, 스타일시트, 이미지도 서버에서 다운로드해야 합니다.

preload 속성을 사용하면 이런 리소스들을 미리 다운로드할 수 있습니다.

이미지를 동적으로 로드하는 예제를 만들어보겠습니다.

<body>
  <button id="imageAddButton">이미지 추가</button>
  <script>
    // 버튼 클릭 시 이미지를 동적으로 추가
    document.querySelector('#imageAddButton').addEventListener('click', () => {
      // img 엘리먼트 생성
      const img = document.createElement('img');
      // 이미지 경로 설정
      img.src = '/goguma.jpg';
      // body에 이미지 추가 (이 시점에 네트워크 요청 발생)
      document.body.appendChild(img);
    });
  </script>
</body>html

server.js에서 goguma.jpg 응답을 1초 지연시켜보겠습니다.

const handler = (req, res) => {
  // filename 가져오기
  const filename = path.basename(req.url);

  if (filename === 'goguma.jpg') {
    // 1초 후 응답
    res.delayMs = 1_000;
  }

  static(path.join(__dirname, 'public'))(req, res);
};js

서버를 실행하면 버튼이 나타나고, 클릭하면 1초 후에 이미지가 표시됩니다.

버튼 클릭 후 1초 후에 고구마 이미지가 보일 것이다.
버튼 클릭 후 1초 후에 고구마 이미지가 보일 것이다.

이미지가 크면 사용자는 버튼 클릭 후 오래 기다려야 합니다.

preload를 사용하면 이미지를 미리 다운로드해서 즉시 표시할 수 있습니다.

head 태그에 다음과 같이 추가합니다.

<head>
  <link rel="preload" href="/goguma.jpg" as="image" />
</head>html

브라우저는 이 태그를 만나면 파싱을 멈추지 않고 리소스를 동시에 다운로드합니다.

미리 이미지를 preload 함
미리 이미지를 preload 함

버튼 클릭 전에 이미지가 미리 다운로드되어, 클릭하면 즉시 표시됩니다.

preload는 이미지, 비디오, 스타일시트, 폰트, 자바스크립트 등 모든 리소스에 사용할 수 있습니다.

핵심 정리: preload는 현재 페이지에서 곧 사용될 중요한 리소스를 미리 다운로드하여 실제 사용 시점의 로딩 시간을 제거합니다.


6. Prefetch 링크

preload로 현재 페이지의 리소스 로딩을 최적화했습니다.

prefetch는 다음 페이지의 리소스를 미리 다운로드합니다. 사용자가 이동할 가능성이 높은 페이지를 개발자가 예측해서 설정합니다.

하이퍼링크를 추가해보겠습니다.

<body>
  <a href="goguma.html">Goguma들이 모인 Page</a>
</body>html

goguma.html 파일을 만들어보겠습니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Goguma들이 모인 페이지</title>
  </head>

  <body>
    <h1>Goguma들이 모인 페이지</h1>
  </body>
</html>html

서버 핸들러에서 3초 지연과 Firefox를 위한 캐시 헤더를 추가합니다.

if (filename === 'goguma.html') {
  // 3초 후 응답
  res.delayMs = 3_000;
  // prefetch를 위한 캐싱 헤더 추가
  // Firefox는 prefetch하기 위해 캐싱 헤더가 필요함
  res.setHeader('Cache-Control', 'max-age=3600');
}js

서버를 다시 실행해보겠습니다.

Goguma들이 모인 Page 버튼이 보임
Goguma들이 모인 Page 버튼이 보임

링크를 클릭하면 3초 후에 페이지가 표시됩니다.

클릭 후 3초 후에 Goguma들이 모인 페이지가 보임
클릭 후 3초 후에 Goguma들이 모인 페이지가 보임

페이지가 크거나 응답이 느리면 사용자는 오래 기다려야 합니다.

prefetch로 다음 페이지를 미리 다운로드할 수 있습니다.

<head>
  <link rel="prefetch" href="goguma.html" as="document" />
</head>html

브라우저는 유휴 시간에 prefetch로 설정한 파일을 다운로드합니다.

이제 다음 페이지의 자원을 미리 다운로드 함
이제 다음 페이지의 자원을 미리 다운로드 함

이제 3초 지연 없이 즉시 페이지가 표시됩니다. 브라우저가 유휴 시간에 미리 다운로드했기 때문입니다.

prefetch는 브라우저 호환성에 제한이 있습니다.

prefetch는 can i use를 보면 브라우저별 제한이 있음
prefetch는 can i use를 보면 브라우저별 제한이 있음

Firefox는 캐시 헤더가 필요하고, Chrome은 별도 설정 없이 동작합니다. Can I Use에서 브라우저 호환성을 확인하세요.

Next.js, Gatsby, webpack 등 많은 프레임워크가 prefetch를 활용합니다.

webpack에서는 주석으로 간단히 설정할 수 있습니다.

import(/* webpackPrefetch: true */ './path/to/LoginModal.js');js

빌드 시 자동으로 link prefetch 태그를 추가합니다. Next.js의 Link 컴포넌트도 prefetch를 기본 제공합니다.

핵심 정리: prefetch는 다음 페이지에서 사용될 리소스를 유휴 시간에 미리 다운로드합니다. 브라우저 호환성을 확인하고, 다음 페이지 예측이 가능한 경우에 사용하면 효과적입니다.


7. 이미지 지연 로딩

이미지가 많으면 그만큼 HTTP 요청이 늘어나 로딩이 느려집니다.

여러 이미지를 로드하는 예제를 만들어보겠습니다.

<body>
  <img src="/goguma.jpg" alt="고구마1 이미지" />
  <img src="/goguma2.jpg" alt="고구마2 이미지" />
</body>html

서버 핸들러에서 두 이미지 모두 1초씩 지연시킵니다.

const handler = () => {
  if (filename === 'goguma.jpg') {
    // 1초 후 응답
    res.delayMs = 1_000;
  }

  if (filename === 'goguma2.jpg') {
    // 1초 후 응답
    res.delayMs = 1_000;
  }
};js

두 번째 이미지를 뷰포트 밖으로 밀어내보겠습니다.

<body>
  <img src="/goguma.jpg" alt="고구마1 이미지" />
  <div style="height: 1000vh; border: solid 1px black">빈 박스</div>
  <img src="/goguma2.jpg" alt="고구마2 이미지" />
</body>html

두 번째 이미지는 뷰포트 밖에 있어 보이지 않습니다.

2번째 고구마 이미지는 뷰포트 밖으로 밀려나서 보이지 않음, 굳이 이걸 로딩할 필요가 있을까?

네트워크 탭을 보면 두 이미지 모두 다운로드됐습니다.

사용자가 보지 않을 수도 있는 두 번째 이미지까지 다운로드하는 것은 비효율적입니다.

뷰포트 밖의 이미지는 스크롤할 때 로드하면 됩니다. 이미지 태그에 loading 속성을 추가해보겠습니다.

<img src="/goguma.jpg" alt="고구마1 이미지" loading="eager" />
<div style="height: 1000vh; border: solid 1px black">빈 박스</div>
<img src="/goguma2.jpg" alt="고구마2 이미지" loading="eager" />html

loading 속성의 기본값은 eager로, 모든 이미지를 즉시 다운로드합니다.

<img src="/goguma.jpg" alt="고구마1 이미지" loading="lazy" />
<div style="height: 1000vh; border: solid 1px black">빈 박스</div>
<img src="/goguma2.jpg" alt="고구마2 이미지" loading="lazy" />html

lazy로 변경하면 뷰포트에 들어올 때만 이미지를 다운로드합니다.

뷰포트 안에 들어올 때 HTTP 요청을 만듬
뷰포트 안에 들어올 때 HTTP 요청을 만듬

첫 번째 이미지만 로드되고, 두 번째 이미지는 스크롤해서 뷰포트에 가까워질 때 로드됩니다.

이미지가 많은 사진첩이나 블로그, 특히 모바일 환경에서 효과적입니다.

핵심 정리: 이미지 태그의 loading="lazy" 속성을 사용하면 뷰포트에 들어올 때만 이미지를 로드하여 초기 렌더링 성능을 크게 개선할 수 있습니다.


마무리

이번 포스팅에서는 브라우저의 렌더링 과정과 최적화 기법들을 알아보았습니다.

  • Critical Rendering Path: 파싱 → DOM/CSSOM → 렌더 트리 → 레이아웃 → 페인팅
  • 스크립트 최적화: asyncdefer 속성으로 파싱 차단 방지
  • 리소스 프리로딩: preloadprefetch로 중요 리소스 미리 다운로드
  • 이미지 지연 로딩: loading="lazy"로 뷰포트 외부 이미지 지연 로드

이러한 기법들을 적절히 활용하면 웹 페이지의 로딩 성능을 크게 개선할 수 있습니다. 특히 모바일 환경이나 네트워크가 느린 환경에서 더욱 효과적입니다.

참고 자료: