스마트 번들링: 레거시 브라우저에만 레거시 코드를 제공하는 방법
게시 됨: 2022-03-10오늘날 웹 사이트는 에버그린 브라우저에서 많은 양의 트래픽을 수신합니다. 대부분은 ES6+, 새로운 JavaScript 표준, 새로운 웹 플랫폼 API 및 CSS 속성을 잘 지원합니다. 그러나 레거시 브라우저는 가까운 장래에 계속 지원되어야 합니다. 사용 점유율은 사용자 기반에 따라 무시할 수 없을 만큼 충분히 큽니다.
caniuse.com의 사용 표를 간략히 살펴보면 에버그린 브라우저가 브라우저 시장의 75% 이상을 차지하는 것으로 나타났습니다. 그럼에도 불구하고 표준은 CSS 접두사를 지정하고 모든 JavaScript를 ES5로 변환하며 관심 있는 모든 사용자를 지원하기 위해 폴리필을 포함하는 것입니다.
이것은 역사적 맥락에서 이해할 수 있지만 – 웹은 항상 점진적인 향상에 관한 것이었습니다 – 질문은 남아 있습니다. 줄어들고 있는 레거시 브라우저 집합을 지원하기 위해 대다수 사용자의 웹 속도를 늦추고 있습니까?
레거시 브라우저 지원 비용
일반적인 빌드 파이프라인의 여러 단계가 프런트 엔드 리소스에 가중치를 추가할 수 있는 방법을 이해해 보겠습니다.
ES5로 변환
트랜스파일이 JavaScript 번들에 추가할 수 있는 가중치를 추정하기 위해 원래 ES6+로 작성된 몇 가지 인기 있는 JavaScript 라이브러리를 가져와서 트랜스파일 전후의 번들 크기를 비교했습니다.
도서관 | 크기 (ES6 축소) | 크기 (ES5 축소) | 차이점 |
---|---|---|---|
TodoMVC | 8.4KB | 11KB | 24.5% |
드래그 가능 | 53.5KB | 77.9KB | 31.3% |
룩슨 | 75.4KB | 100.3KB | 24.8% |
비디오.js | 237.2KB | 335.8KB | 29.4% |
픽시JS | 370.8KB | 452KB | 18% |
평균적으로 트랜스파일되지 않은 번들은 ES5로 트랜스파일된 번들보다 약 25% 더 작습니다. ES6+가 동등한 논리를 표현하는 보다 간결하고 표현적인 방법을 제공하고 이러한 기능 중 일부를 ES5로 변환하는 데 많은 코드가 필요할 수 있다는 점을 감안할 때 이것은 놀라운 일이 아닙니다.
ES6+ 폴리필
Babel은 ES6+ 코드에 구문 변환을 잘 적용하지만 Promise
, Map
및 Set
, 새로운 배열 및 문자열 메서드와 같은 ES6+에 도입된 내장 기능은 여전히 폴리필해야 합니다. babel-polyfill
을 있는 그대로 드롭하면 축소된 번들에 거의 90KB를 추가할 수 있습니다.
웹 플랫폼 폴리필
최신 웹 애플리케이션 개발은 새로운 브라우저 API의 과다 사용으로 인해 단순화되었습니다. 일반적으로 사용되는 것은 리소스 요청을 위한 fetch
, 요소의 가시성을 효율적으로 관찰하기 위한 IntersectionObserver
, 웹에서 URL을 더 쉽게 읽고 조작할 수 있게 해주는 URL
사양입니다.
이러한 각 기능에 대해 사양 준수 폴리필을 추가하면 번들 크기에 상당한 영향을 미칠 수 있습니다.
CSS 접두사
마지막으로 CSS 접두사의 영향을 살펴보겠습니다. 접두사는 다른 빌드 변환만큼 번들에 가중치를 추가하지 않지만(특히 Gzip으로 압축할 때 압축이 잘 되기 때문에) 여기에서 얻을 수 있는 약간의 절감 효과가 있습니다.
도서관 | 크기 (최소화, 마지막 5개 브라우저 버전에 대한 접두사) | 크기 (최소화, 마지막 브라우저 버전의 접두사) | 차이점 |
---|---|---|---|
부트스트랩 | 159KB | 132KB | 17% |
부르마 | 184KB | 164KB | 10.9% |
기초 | 139KB | 118KB | 15.1% |
시맨틱 UI | 622KB | 569KB | 8.5% |
효율적인 코드 발송을 위한 실용적인 가이드
내가 이것을 가지고 어디로 가고 있는지는 아마도 분명합니다. 기존 빌드 파이프라인을 활용하여 이러한 호환성 레이어를 필요로 하는 브라우저에만 제공한다면, 이전 브라우저에 대한 호환성을 유지하면서 나머지 사용자(다수가 증가하는 사용자)에게 더 가벼운 경험을 제공할 수 있습니다.
이 아이디어는 완전히 새로운 것은 아닙니다. Polyfill.io와 같은 서비스는 런타임에 브라우저 환경을 동적으로 폴리필하려는 시도입니다. 그러나 이와 같은 접근 방식에는 몇 가지 단점이 있습니다.
- 서비스를 직접 호스팅하고 유지 관리하지 않는 한 폴리필의 선택은 서비스에서 나열한 것으로 제한됩니다.
- 폴리필은 런타임에 발생하고 차단 작업이기 때문에 페이지 로드 시간은 이전 브라우저 사용자의 경우 훨씬 더 길어질 수 있습니다.
- 모든 사용자에게 맞춤형 폴리필 파일을 제공하면 시스템에 엔트로피가 발생하여 문제가 발생할 때 문제를 해결하기가 더 어려워집니다.
또한 이것은 때때로 폴리필 자체보다 클 수 있는 애플리케이션 코드의 변환으로 인해 추가되는 가중치 문제를 해결하지 못합니다.
지금까지 확인된 모든 팽창의 원인을 해결할 수 있는 방법을 살펴보겠습니다.
필요한 도구
- 웹팩
이 프로세스는 Parcel 및 Rollup과 같은 다른 빌드 도구의 프로세스와 유사하게 유지되지만 이것이 우리의 빌드 도구가 될 것입니다. - 브라우저 목록
이를 통해 지원하려는 브라우저를 관리하고 정의합니다. - 그리고 몇몇 Browserslist 지원 플러그인 을 사용할 것입니다.
1. 최신 및 레거시 브라우저 정의
먼저 "현대" 및 "레거시" 브라우저가 의미하는 바를 명확히 하고 싶습니다. 유지 관리 및 테스트의 용이성을 위해 브라우저를 두 개의 개별 그룹으로 나누는 것이 도움이 됩니다. 폴리필 또는 변환이 거의 또는 전혀 필요하지 않은 브라우저를 최신 목록에 추가하고 나머지는 레거시 목록에 추가합니다.
프로젝트 루트의 Browserslist 구성은 이 정보를 저장할 수 있습니다. "Environment" 하위 섹션은 다음과 같이 두 개의 브라우저 그룹을 문서화하는 데 사용할 수 있습니다.
[modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%
여기에 제공된 목록은 예시일 뿐이며 웹사이트의 요구 사항과 사용 가능한 시간에 따라 사용자 정의하고 업데이트할 수 있습니다. 이 구성은 다음에 생성할 두 가지 프론트 엔드 번들 세트에 대한 정보 소스 역할을 합니다. 하나는 최신 브라우저용이고 다른 하나는 다른 모든 사용자용입니다.
2. ES6+ 트랜스파일과 폴리필링
환경 인식 방식으로 JavaScript를 변환하기 위해 babel-preset-env
를 사용할 것입니다.
다음과 같이 프로젝트 루트에서 .babelrc
파일을 초기화해 보겠습니다.
{ "presets": [ ["env", { "useBuiltIns": "entry"}] ] }
useBuiltIns
플래그를 활성화하면 Babel이 ES6+의 일부로 도입된 내장 기능을 선택적으로 폴리필할 수 있습니다. 환경에서 필요한 것만 포함하도록 폴리필을 필터링하기 때문에 babel-polyfill
전체로 배송 비용을 절감합니다.
이 플래그가 작동하려면 진입점에서 babel-polyfill
도 가져와야 합니다.
// In import "babel-polyfill";
그렇게 하면 큰 babel-polyfill
가져오기가 우리가 대상으로 하는 브라우저 환경에 의해 필터링된 세분화된 가져오기로 대체됩니다.
// Transformed output import "core-js/modules/es7.string.pad-start"; import "core-js/modules/es7.string.pad-end"; import "core-js/modules/web.timers"; …
3. Polyfilling 웹 플랫폼 기능
웹 플랫폼 기능용 폴리필을 사용자에게 제공하려면 두 환경 모두에 대해 두 개의 진입점을 만들어야 합니다.
require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills
이:
// polyfills for modern browsers (if any) require('intersection-observer');
이것은 어느 정도의 수동 유지 관리가 필요한 흐름의 유일한 단계입니다. eslint-plugin-compat를 프로젝트에 추가하여 이 프로세스를 오류가 덜 발생하도록 만들 수 있습니다. 이 플러그인은 아직 폴리필되지 않은 브라우저 기능을 사용할 때 경고합니다.
4. CSS 접두사
마지막으로 CSS 접두사가 필요하지 않은 브라우저에서 CSS 접두사를 줄이는 방법을 살펴보겠습니다. autoprefixer
는 browserslist
구성 파일에서 읽기를 지원하는 에코시스템의 첫 번째 도구 중 하나였기 때문에 여기서 할 일이 많지 않습니다.
프로젝트 루트에서 간단한 PostCSS 구성 파일을 만드는 것으로 충분합니다.
module.exports = { plugins: [ require('autoprefixer') ], }
함께 모아서
이제 필요한 모든 플러그인 구성을 정의했으므로 이를 읽고 dist/modern
및 dist/legacy
폴더에 두 개의 개별 빌드를 출력하는 webpack 구성을 함께 넣을 수 있습니다.
const MiniCssExtractPlugin = require('mini-css-extract-plugin') const isModern = process.env.BROWSERSLIST_ENV === 'modern' const buildRoot = path.resolve(__dirname, "dist") module.exports = { entry: [ isModern ? './polyfills.modern.js' : './polyfills.legacy.js', "./main.js" ], output: { path: path.join(buildRoot, isModern ? 'modern' : 'legacy'), filename: 'bundle.[hash].js', }, module: { rules: [ { test: /\.jsx?$/, use: "babel-loader" }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] } ]}, plugins: { new MiniCssExtractPlugin(), new HtmlWebpackPlugin({ template: 'index.hbs', filename: 'index.html', }), }, };
마무리를 위해 package.json
파일에 몇 가지 빌드 명령을 생성합니다.
"scripts": { "build": "yarn build:legacy && yarn build:modern", "build:legacy": "BROWSERSLIST_ENV=legacy webpack -p --config webpack.config.js", "build:modern": "BROWSERSLIST_ENV=modern webpack -p --config webpack.config.js" }
그게 다야 이제 yarn build
를 실행하면 기능면에서 동일한 두 가지 빌드가 제공됩니다.
사용자에게 적합한 번들 제공
별도의 빌드를 생성하면 목표의 전반부만 달성할 수 있습니다. 우리는 여전히 올바른 번들을 식별하고 사용자에게 제공해야 합니다.
앞에서 정의한 Browserlist 구성을 기억하십니까? 동일한 구성을 사용하여 사용자가 속하는 범주를 결정할 수 있다면 좋지 않을까요?
브라우저 목록-사용자 에이전트를 입력합니다. 이름에서 알 수 있듯이 browserslist-useragent
는 browserslist
구성을 읽은 다음 사용자 에이전트를 관련 환경과 일치시킬 수 있습니다. 다음 예는 Koa 서버에서 이를 보여줍니다.
const Koa = require('koa') const app = new Koa() const send = require('koa-send') const { matchesUA } = require('browserslist-useragent') var router = new Router() app.use(router.routes()) router.get('/', async (ctx, next) => { const useragent = ctx.get('User-Agent') const isModernUser = matchesUA(useragent, { env: 'modern', allowHigherVersions: true, }) const index = isModernUser ? 'dist/modern/index.html', 'dist/legacy/index.html' await send(ctx, index); });
여기에서 allowHigherVersions
플래그를 설정하면 아직 Can I Use의 데이터베이스에 포함되지 않은 최신 버전의 브라우저가 출시되더라도 최신 브라우저에 대해 여전히 진실한 것으로 보고됩니다.
browserslist-useragent
의 기능 중 하나는 사용자 에이전트를 일치시키는 동안 플랫폼의 단점이 고려되도록 하는 것입니다. 예를 들어 iOS의 모든 브라우저(Chrome 포함)는 WebKit을 기본 엔진으로 사용하며 각 Safari 관련 Browserslist 쿼리와 일치합니다.
프로덕션에서 사용자 에이전트 구문 분석의 정확성에만 의존하는 것은 현명하지 않을 수 있습니다. 최신 목록에 정의되지 않았거나 알 수 없거나 구문 분석할 수 없는 사용자 에이전트 문자열이 있는 브라우저의 경우 레거시 번들로 폴백하여 웹사이트가 계속 작동하도록 합니다.
결론: 가치가 있습니까?
우리는 고객에게 팽만감 없는 번들을 배송하기 위한 종단 간 흐름을 처리했습니다. 그러나 이것이 프로젝트에 추가되는 유지 관리 오버헤드가 그 혜택을 받을 가치가 있는지 궁금해하는 것은 합리적일 뿐입니다. 이 접근 방식의 장단점을 평가해 보겠습니다.
1. 유지보수 및 테스트
하나는 이 파이프라인의 모든 도구를 지원하는 단일 Browserslist 구성만 유지하는 데 필요합니다. 최신 및 레거시 브라우저의 정의 업데이트는 지원 구성 또는 코드를 리팩토링할 필요 없이 향후 언제든지 수행할 수 있습니다. 이것이 유지 관리 오버헤드를 거의 무시할 수 있게 한다고 주장합니다.
그러나 각각의 환경에서 제대로 작동해야 하는 두 개의 서로 다른 코드 번들을 생성하기 위해 Babel에 의존하는 것과 관련된 작은 이론적 위험이 있습니다.
번들의 차이로 인한 오류는 드물지만 이러한 변형에 오류가 있는지 모니터링하면 문제를 식별하고 효과적으로 완화하는 데 도움이 됩니다.
2. 빌드 시간 대 런타임
오늘날 널리 사용되는 다른 기술과 달리 이러한 모든 최적화는 빌드 시 발생하며 클라이언트에는 표시되지 않습니다.
3. 점진적으로 향상된 속도
최신 브라우저를 사용하는 사용자의 경험은 훨씬 더 빨라지는 반면, 기존 브라우저의 사용자는 부정적인 결과 없이 이전과 동일한 번들을 계속 제공받습니다.
4. 최신 브라우저 기능을 쉽게 사용하기
우리는 종종 새로운 브라우저 기능을 사용하는 데 필요한 폴리필의 크기 때문에 사용을 기피합니다. 때때로 우리는 크기를 줄이기 위해 더 작은 규격을 준수하지 않는 폴리필을 선택하기도 합니다. 이 새로운 접근 방식을 통해 모든 사용자에게 영향을 미치는 것에 대해 크게 걱정하지 않고 사양 준수 폴리필을 사용할 수 있습니다.
프로덕션에서 제공되는 차등 번들
상당한 이점을 감안할 때 우리는 인도 최대의 가구 및 장식 소매업체 중 하나인 Urban Ladder의 고객을 위한 새로운 모바일 체크아웃 경험을 만들 때 이 빌드 파이프라인을 채택했습니다.
이미 최적화된 번들에서 현대 모바일 사용자에게 유선으로 전송되는 Gzip으로 압축된 CSS 및 JavaScript 리소스를 약 20% 절약할 수 있었습니다. 일일 방문자의 80% 이상이 이러한 에버그린 브라우저를 사용했기 때문에 투입된 노력은 그만한 가치가 있었습니다.
추가 리소스
- "필요할 때만 폴리필 로드", Philip Walton
-
@babel/preset-env
스마트한 Babel 사전 설정 - 브라우저 목록 "도구"
Browserslist용으로 구축된 플러그인 생태계 - 사용해도 되나요
현재 브라우저 시장 점유율 표