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

이 Codelab에서는 사용자가 무작위 고양이를 평가할 수 있는 이 간단한 애플리케이션의 성능을 개선합니다. 트랜스파일되는 코드의 양을 최소화하여 JavaScript 번들을 최적화하는 방법을 알아봅니다.

앱 스크린샷

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

측정

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

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

원래 번들 크기 요청

이 애플리케이션에 80KB 이상이 사용됩니다. 번들의 일부가 사용되지 않는지 확인하는 시간:

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

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

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

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

  4. 기본 번들에 사용된 코드와 로드된 코드를 비교해 보세요.

    번들의 코드 적용 범위

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

@babel/preset-env 사용

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

애플리케이션에서 사용되는 ES2015 기능은 다음과 같습니다.

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

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

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

  • 폴리필은 최신 ES2015+ 함수를 에뮬레이션하여 브라우저에서 지원하지 않더라도 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 기능이 지원되지 않는 환경에서 작동할 수 있도록 필요한 모든 폴리필을 제공합니다. 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는 browserslist와 통합되므로 이 필드에서 사용할 수 있는 호환 쿼리의 전체 목록은 browserlist 문서에서 확인할 수 있습니다.

"last 2 versions" 값은 모든 브라우저의 최신 두 버전에 맞게 애플리케이션의 코드를 트랜스파일합니다.

디버깅

포함된 모든 변환 및 polyfill뿐만 아니라 모든 브라우저의 Babel 타겟을 완전히 확인하려면 .babelrc:debug 필드를 추가하세요.

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

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

타겟팅된 브라우저

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

타겟팅된 브라우저

Internet Explorer와 같이 지원이 중단된 브라우저가 이 목록에 포함되어 있습니다. 지원되지 않는 브라우저에는 새로운 기능이 추가되지 않으며 Babel은 계속해서 특정 구문을 트랜스파일하므로 문제가 됩니다. 사용자가 이 브라우저를 사용하여 사이트에 액세스하지 않는 경우 번들의 크기가 불필요하게 증가합니다.

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

사용된 플러그인 목록

목록이 상당히 길죠. 이러한 플러그인은 Babel이 모든 타겟팅 브라우저에 대해 ES2015 이상의 구문을 이전 구문으로 변환하는 데 필요한 모든 플러그인입니다.

하지만 Babel은 사용된 특정 폴리필을 표시하지 않습니다.

추가된 polyfill 없음

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

polyfill을 개별적으로 로드

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

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

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

가져온 polyfill 목록

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

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

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

import "./style.css";
import "@babel/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 기능은 이미 JavaScript 모듈을 지원하는 환경에서 지원됩니다 (Babel이 필요하지 않음). 즉, Babel 구성을 수정하여 애플리케이션의 두 가지 버전을 브라우저로 보낼 수 있습니다.

  • 모듈을 지원하고 대부분 트랜스파일되지 않았지만 파일 크기가 더 작은 모듈이 포함된 최신 브라우저에서 작동하는 버전
  • 모든 기존 브라우저에서 작동하는 더 큰 트랜스파일된 스크립트가 포함된 버전

Babel로 ES 모듈 사용

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

먼저 기존 스크립트의 구성을 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이 포함됩니다.

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

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>

여기서 마지막으로 해야 할 일은 모듈과 기존 스크립트에 각각 modulenomodule 속성을 추가하고 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를 사용하여 타겟팅된 브라우저에 필요한 폴리필만 제공하는 방법을 이해합니다. 또한 JavaScript 모듈이 애플리케이션의 트랜스파일된 두 가지 버전을 제공하여 성능을 더욱 개선할 수 있다는 것도 알고 있습니다. 이 두 기법이 번들 크기를 크게 줄일 수 있는 방법을 어느 정도 이해했다면 이제 최적화하세요.