페이지 로드 속도를 높이기 위해 최신 브라우저에 최신 코드 제공하기

후세인 지르데
후세인 지르데

이 Codelab에서는 사용자가 임의의 고양이를 평가할 수 있는 간단한 애플리케이션의 성능을 개선합니다. 변환 컴파일되는 코드 양을 최소화하여 JavaScript 번들을 최적화하는 방법을 알아보세요.

앱 스크린샷

샘플 앱에서 각 고양이가 얼마나 좋아하는지 나타내는 단어나 그림 이모티콘을 선택할 수 있습니다. 버튼을 클릭하면 앱이 현재 고양이 이미지 아래에 버튼 값을 표시합니다.

측정

최적화를 추가하기 전에 항상 웹사이트를 검사하는 것이 좋습니다.

  1. 사이트를 미리 보려면 View App을 누릅니다. 그런 다음 Fullscreen 전체 화면을 누릅니다.
  2. `Control+Shift+J` (Mac의 경우 `Command+Option+J`)를 눌러 DevTools를 엽니다.
  3. 네트워크 탭을 클릭합니다.
  4. 캐시 사용 중지 체크박스를 선택합니다.
  5. 앱을 새로고침합니다.

원래 번들 크기 요청

이 애플리케이션에는 80KB 이상이 사용되었습니다. 번들의 일부가 사용되지 않는지 확인합니다.

  1. Control+Shift+P (Mac의 경우 Command+Shift+P)를 눌러 Command 메뉴를 엽니다. 명령어 메뉴

  2. Show Coverage를 입력하고 Enter를 눌러 색인 생성 범위 탭을 표시합니다.

  3. 커버리지 탭에서 새로고침을 클릭하여 커버리지를 캡처하는 동안 애플리케이션을 새로고침합니다.

    코드 적용 범위로 앱 새로고침

  4. 기본 번들에 사용된 코드 및 로드된 코드의 양을 살펴보세요.

    번들의 코드 적용 범위

번들의 절반 이상 (44KB)은 사용되지 않습니다. 그 이유는 애플리케이션이 이전 브라우저에서 작동하도록 하는 코드가 폴리필로 구성되어 있기 때문입니다.

@babel/preset-env 사용

자바스크립트 언어의 구문은 ECMAScript 또는 ECMA-262라는 표준을 준수합니다. 사양의 최신 버전은 매년 출시되며 제안서 절차를 통과한 새로운 기능을 포함합니다. 각각의 주요 브라우저는 이러한 기능을 지원하는 단계가 항상 다릅니다

다음 ES2015 기능이 애플리케이션에서 사용됩니다.

다음 ES2017 기능도 사용됩니다.

자유롭게 src/index.js의 소스 코드를 살펴보고 모두 어떻게 사용되는지 확인하세요.

이러한 기능은 모두 최신 버전의 Chrome에서 지원되지만 이 기능을 지원하지 않는 다른 브라우저는 어떻게 되나요? 애플리케이션에 포함된 Babel은 최신 문법을 포함하는 코드를 이전 브라우저와 환경에서 이해할 수 있는 코드로 컴파일하는 데 가장 많이 사용되는 라이브러리입니다. 이 작업은 다음과 같은 두 가지 방식으로 이루어집니다.

  • 최신 ES2015+ 함수를 에뮬레이션하기 위해 Polyfill이 포함되므로 브라우저에서 지원하지 않는 경우에도 API를 사용할 수 있습니다. 다음은 Array.includes 메서드의 polyfill에 관한 예입니다.
  • 플러그인은 ES2015 코드 (또는 이후 버전)를 이전 ES5 구문으로 변환하는 데 사용됩니다. 이는 구문 관련 변경사항 (예: 화살표 함수)이므로 폴리필을 사용하여 에뮬레이션할 수 없습니다.

package.json를 보고 어떤 Babel 라이브러리가 포함되어 있는지 확인합니다.

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}
  • @babel/core는 핵심 Babel 컴파일러입니다. 이렇게 하면 모든 Babel 구성이 프로젝트 루트에 있는 .babelrc에서 정의됩니다.
  • babel-loader에는 webpack 빌드 프로세스에 Babel이 포함되어 있습니다.

이제 webpack.config.js를 보고 babel-loader가 규칙으로 포함되는 방법을 확인합니다.

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill는 최신 ECMAScript 기능을 지원하지 않는 환경에서도 작동할 수 있도록 필요한 모든 polyfill을 제공합니다. 이미 src/index.js.의 맨 위에 가져왔습니다.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env는 타겟으로 선택된 브라우저 또는 환경에 필요한 변환 및 폴리필을 식별합니다.

Babel 구성 파일 .babelrc를 살펴보고 포함되는 방법을 확인합니다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

Babel 및 webpack 설정입니다. webpack과 다른 모듈 번들러를 사용하는 경우 애플리케이션에 Babel을 포함하는 방법을 알아보세요.

.babelrctargets 속성은 타겟팅 중인 브라우저를 식별합니다. @babel/preset-env는 Browserlist와 통합됩니다. 즉, browserlist 문서에서 이 필드에서 사용할 수 있는 호환 가능한 쿼리의 전체 목록을 확인할 수 있습니다.

"last 2 versions" 값은 애플리케이션에서 모든 브라우저의 마지막 두 버전에 대한 코드를 변환 컴파일합니다.

디버깅

브라우저의 모든 Babel 타겟과 포함된 모든 변환 및 polyfill을 완전히 살펴보려면 debug 필드를 .babelrc:에 추가합니다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • 도구를 클릭합니다.
  • 로그를 클릭합니다.

애플리케이션을 새로고침하고 편집기 하단의 Glitch 상태 로그를 확인합니다.

타겟팅된 브라우저

Babel은 코드가 컴파일된 모든 대상 환경을 비롯하여 컴파일 프로세스에 대한 여러 세부정보를 콘솔에 로깅합니다.

타겟팅된 브라우저

Internet Explorer와 같이 지원 중단된 브라우저가 이 목록에 포함된 방식을 확인하세요. 이는 지원되지 않는 브라우저에는 최신 기능이 추가되지 않고 Babel이 이러한 기능의 특정 구문을 계속해서 트랜스파일하기 때문에 문제가 됩니다. 이로 인해 사용자가 이 브라우저를 사용하여 사이트에 액세스하지 않는 경우 번들 크기가 불필요하게 증가합니다.

Babel은 사용된 변환 플러그인의 목록도 기록합니다.

사용된 플러그인 목록

꽤 긴 목록이에요. 이는 Babel이 모든 타겟팅된 브라우저에서 ES2015+ 구문을 이전 구문으로 변환하는 데 필요한 모든 플러그인입니다.

그러나 Babel은 사용되는 특정 polyfill을 표시하지 않습니다.

추가된 폴리필 없음

전체 @babel/polyfill를 직접 가져오기 때문입니다.

개별적으로 폴리필 로드

기본적으로 Babel은 @babel/polyfill을 파일로 가져오면 완전한 ES2015+ 환경에 필요한 모든 폴리필을 포함합니다. 타겟 브라우저에 필요한 특정 polyfill을 가져오려면 구성에 useBuiltIns: 'entry'을 추가하세요.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

애플리케이션을 새로고침합니다. 이제 포함된 모든 특정 polyfill을 확인할 수 있습니다.

가져온 polyfill 목록

이제 "last 2 versions"에 필요한 폴리필만 포함되었지만 여전히 매우 긴 목록입니다. 이는 모든 최신 기능의 타겟 브라우저에 필요한 polyfill이 여전히 포함되어 있기 때문입니다. 코드에서 사용 중인 기능에 필요한 항목만 포함하도록 속성 값을 usage로 변경합니다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

이렇게 하면 필요한 곳에 폴리필이 자동으로 포함됩니다. 즉, src/index.js.에서 @babel/polyfill 가져오기를 삭제할 수 있습니다.

import "./style.css";
import "@babel/polyfill";

이제 애플리케이션에 필요한 필수 폴리필만 포함되었습니다.

자동으로 포함되는 polyfill 목록

애플리케이션 번들 크기가 크게 줄어듭니다.

번들 크기가 30.1KB로 감소

지원되는 브라우저 목록 범위 축소

포함된 브라우저 타겟의 수는 여전히 많으며 Internet Explorer와 같이 중단된 브라우저를 사용하는 사용자는 많지 않습니다. 구성을 다음과 같이 업데이트합니다.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

가져온 번들의 세부정보를 살펴봅니다.

번들 크기 30.0KB

애플리케이션이 매우 작기 때문에 이러한 변경사항에 큰 차이가 없습니다. 그러나 사용자가 사용하지 않는다고 확신하는 특정 브라우저를 제외하는 것과 함께 브라우저 시장점유율 (예: ">0.25%")을 사용하는 것이 좋습니다. 자세한 내용은 제임스 카일의 유해한 것으로 간주되는 '최근 2개 버전' 도움말을 참고하세요.

<script type="module"> 사용

아직도 개선의 여지가 있습니다. 사용되지 않는 폴리필이 여러 개 삭제되었지만, 일부 브라우저에는 필요하지 않은 많은 폴리필이 제공됩니다. 모듈을 사용하면 불필요한 폴리필을 사용하지 않고도 최신 문법을 직접 작성하여 브라우저에 제공할 수 있습니다.

JavaScript 모듈모든 주요 브라우저에서 지원되는 비교적 새로운 기능입니다. type="module" 속성을 사용하여 모듈을 만들어 다른 모듈에서 가져오고 내보내는 스크립트를 정의할 수 있습니다. 예를 들면 다음과 같습니다.

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

여러 최신 ECMAScript 기능은 자바스크립트 모듈을 지원하는 환경에서 이미 지원되고 있습니다 (Babel이 필요 없음). 즉, 두 가지 버전의 애플리케이션을 브라우저에 전송하도록 Babel 구성을 수정할 수 있습니다.

  • 모듈을 지원하는 최신 브라우저에서 작동하고 주로 변환 컴파일되지 않지만 파일 크기가 작은 모듈을 포함하는 버전
  • 모든 기존 브라우저에서 작동할 수 있는 더 큰 트랜스파일된 스크립트가 포함된 버전

Babel과 함께 ES 모듈 사용

애플리케이션의 두 버전에 별도의 @babel/preset-env 설정을 적용하려면 .babelrc 파일을 삭제합니다. 각 애플리케이션 버전에 대해 두 가지 컴파일 형식을 지정하여 webpack 구성에 Babel 설정을 추가할 수 있습니다.

먼저 기존 스크립트의 구성을 webpack.config.js에 추가합니다.

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

"@babel/preset-env"targets 값을 사용하는 대신 값이 falseesmodules가 대신 사용됩니다. 즉, Babel에는 아직 ES 모듈을 지원하지 않는 모든 브라우저를 타겟팅하는 데 필요한 모든 변환 및 polyfill이 포함되어 있습니다.

entry, cssRule, corePlugins 객체를 webpack.config.js 파일 시작 부분에 추가합니다. 이는 모두 브라우저에 제공되는 모듈과 기존 스크립트 간에 공유됩니다.

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

이제 마찬가지로 legacyConfig가 정의된 아래의 모듈 스크립트의 구성 객체를 만듭니다.

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

여기서 주요 차이점은 출력 파일 이름에 .mjs 파일 확장자가 사용된다는 점입니다. 여기서 esmodules 값은 true로 설정됩니다. 즉, 이 모듈에 출력되는 코드는 더 작고 컴파일되지 않은 스크립트이며 사용된 모든 기능이 모듈을 지원하는 브라우저에서 이미 지원되기 때문에 이 예에서는 변환을 거치지 않습니다.

파일 끝에서 두 구성을 단일 배열로 내보냅니다.

module.exports = [
  legacyConfig, moduleConfig
];

이제 이를 지원하는 브라우저를 위한 작은 모듈과 이전 브라우저를 위한 더 큰 트랜스파일된 스크립트가 모두 빌드됩니다.

모듈을 지원하는 브라우저는 nomodule 속성이 있는 스크립트를 무시합니다. 반대로 모듈을 지원하지 않는 브라우저는 type="module"가 있는 스크립트 요소를 무시합니다. 즉, 모듈과 컴파일된 대체를 포함할 수 있습니다. 애플리케이션의 두 버전이 다음과 같이 index.html에 있는 것이 이상적입니다.

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

모듈을 지원하는 브라우저는 main.mjs를 가져와서 실행하고 main.bundle.js.를 무시하며 모듈을 지원하지 않는 브라우저는 그 반대로 이루어집니다.

일반 스크립트와 달리 모듈 스크립트는 기본적으로 항상 지연된다는 점에 유의해야 합니다. 동등한 nomodule 스크립트도 지연되고 파싱된 후에만 실행되도록 하려면 defer 속성을 추가해야 합니다.

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

마지막으로 여기서 해야 할 작업은 module 속성과 nomodule 속성을 각각 모듈과 레거시 스크립트에 추가하는 것입니다. webpack.config.js 맨 위에 ScriptExtHtmlWebpackPlugin을 가져옵니다.

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

이제 이 플러그인을 포함하도록 구성에서 plugins 배열을 업데이트합니다.

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

이러한 플러그인 설정은 모든 .mjs 스크립트 요소의 type="module" 속성을 추가하고 모든 .js 스크립트 모듈의 nomodule 속성을 추가합니다.

HTML 문서의 모듈 제공

마지막으로 해야 할 작업은 기존 스크립트 요소와 최신 스크립트 요소를 모두 HTML 파일로 출력하는 것입니다. 안타깝게도 최종 HTML 파일인 HTMLWebpackPlugin를 생성하는 플러그인은 모듈 스크립트와 nomodule 스크립트의 출력을 현재 지원하지 않습니다. 이 문제를 해결하기 위해 BabelMultiTargetPluginHTMLWebpackMultiBuildPlugin과 같이 해결 방법과 별도의 플러그인을 사용할 수도 있지만, 이 가이드에서는 모듈 스크립트 요소를 수동으로 추가하는 간단한 방법을 사용합니다.

파일 끝의 src/index.js에 다음을 추가합니다.

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

이제 최신 버전의 Chrome과 같은 모듈을 지원하는 브라우저에서 애플리케이션을 로드합니다.

최신 브라우저용으로 네트워크를 통해 가져온 5.2KB 모듈

모듈만 가져오며, 모듈이 크게 변환되지 않기 때문에 번들 크기가 훨씬 작습니다. 다른 스크립트 요소는 브라우저에서 완전히 무시됩니다.

이전 브라우저에서 애플리케이션을 로드하면 필요한 모든 폴리필과 변환이 포함된 더 큰 트랜스파일 스크립트만 가져옵니다. 다음은 이전 버전의 Chrome (버전 38)에서 전송된 모든 요청의 스크린샷입니다.

이전 브라우저용으로 30KB 스크립트를 가져옴

결론

이제 @babel/preset-env를 사용하여 타겟팅된 브라우저에 필요한 필요한 polyfill만 제공하는 방법을 알게 되었습니다. 또한 2개의 트랜스파일된 애플리케이션 버전을 제공하여 JavaScript 모듈이 성능을 더욱 개선하는 방법도 배웠습니다. 이 두 가지 기법이 어떻게 번들 크기를 크게 줄일 수 있는지 충분히 이해했으니 이제 최적화해 보겠습니다.