Uber 의 PWA 는 2G에서도 빠르게 작동하도록 설계되었습니다. 핵심 응용프로그램은 Gk로 50k에 불과하며 2G 네트워크에 로드하는데 3초도 걸리지 않습니다.
Building m.uber : 세계 시장을 위한 고성능 웹 응용 프로그램 엔지니어링(Building m.uber: Engineering a High-Performance Web App for the Global Market) 에서 번역 발췌하였습니다.

( 이미지 출처 : Building m.uber: Engineering a High-Performance Web App for the Global Market )
Uber가 새로운 시장으로 확장함에 따라 모든 사용자가 위치, 네트워크 속도 및 장치에 관계없이 신속하게 승차를 요청(request a ride)할 수 있습니다. 이를 염두에 두고 Google은 웹 클라이언트를 처음부터 native mobile app(기본 모바일 응용 프로그램)의 실행 가능한 대안으로 재 구축했습니다.
모든 최신 브라우저와 호환되는 m.uber 는 기본 클라이언트가 지원하지 않는 기기를 포함하여 로우 엔드 기기(low-end devices, 2G를 이용하는 저사양의 장치들)를 사용하는 라이더에게 앱과 유사한 사용환경(an app-like experience)을 제공합니다. 응용 프로그램도 매우 작습니다. 코어 라이드 요청 응용 프로그램은 50kB에서 제공되므로 2G 네트워크에서도 응용 프로그램을 빠르게 로드 할 수 있습니다.
이 기사에서는 m.uber (moo-ber라고 발음)를 제작하고 초경량 웹 앱에서 네이티브 앱 환경을 구현하는 방법에 대해 설명합니다.
( 이미지 출처 : Building m.uber: Engineering a High-Performance Web App for the Global Market )
m.uber는 ES2015 +로 작성되었으며 Babel for ES5 transpilation을 사용합니다. 가장 중요한 디자인 과제는 기본 앱의 풍부한 경험을 유지하면서 클라이언트의 공간을 최소화하는 것이었습니다. 따라서 우리의 전통적인 아키텍처가 React ( Redux 사용 ) 및 Browserify를 사용 하여 모듈 번들링을 수행하는 동안 우리는 Preact 에서 크기 이점을, Webpack 은 동적 번들 분할 및 트리 – 떨림 기능( tree-shaking capabilities )을 대신했습니다 . 아래에서는 애플리케이션 아키텍처 전반에서 이러한 문제와 다른 문제를 어떻게 해결했는지 설명합니다.
클라이언트는 모든 핵심 자바 스크립트 번들을 다운로드 할 때까지 마크 업 렌더링을 시작할 수 없으므로 m.uber는 Preact를 서버에 렌더링하여 초기 브라우저 요청에 응답합니다. 결과 상태 및 마크 업은 서버 응답의 문자열로 인라인되어 콘텐츠가 거의 즉시 로드됩니다.
m.uber의 목표는 사용자가 최대한 빨리 주행을 요청할 수 있도록하는 것이지만 JavaScript의 대부분은 지불 옵션 업데이트, 여행 진행 상황 확인 또는 설정 편집과 같은 부수적인 작업을 위한 것입니다. 우리가 필요로 하는 자바 스크립트만을 제공하기 위해, 코드 분할을 위해 Webpack을 사용합니다.
비동기 구성 요소에 래핑 된 보조 번들을 반환하는 splitPage 함수를 사용합니다 . 예를 들어 설정 페이지는 아래 함수에 의해 호출됩니다.
const AsyncSettings = splitPage (
{load : () => import ( ‘../../screen/ settings’)}
);
이 함수를 사용하면 AsyncSettings 가 부모 렌더링 메서드에 조건부로 포함된 경우에만 설정 번들을 가져올 수 있습니다 . 아주 느린 연결의 경우, AsyncSettings 는 번들 페치가 완료 될 때까지 “로드 중” 모달을 렌더링합니다.
m.uber는 2G 네트워크에서도 빠르게 설계되므로 클라이언트 크기가 중요합니다. 우리의 핵심 어플리케이션 (당신이 타고 요청할 수있는 애플 리케이션의 필수 부분)은 단지 2GB (250kB / s, 300ms 대기 시간) 네트워크에서 상호 작용하는 3 초의 시간이 걸리고 gzip 및 minified 된 단지 50kB로 제공됩니다. (Our core app (the essential part of the app that allows you to request a ride) comes in at just 50kB gzipped and minified, which means a three second time to interaction on typical 2G (250kB/
아래에서 저희는 m.uber 프로젝트 시작 시점과 현재의 벤더 번들 크기와 종속성의 차이를 강조하여 표시하였습니다.
( 이미지 출처 : Building m.uber: Engineering a High-Performance Web App for the Global Market )
크기면에서 React (45kB)보다 Preact (3kB GZip / minified)를 선택했습니다. Preact는 React가 하는 거의 모든 작업을 수행 할 수 있으며 PropTypes 또는 합성 이벤트를 지원하지 않으며 자체의 몇 가지 멋진 리플렉션 기능을 추가합니다. Preact는 구성 요소와 요소를 재활용 할 때 조금 지나치게 열중합니다 ( 그러나 이 요소로 작업하고 있습니다 ). 이는 예상하지 못한 요소에 대해 키를 정의해야한다는 것을 의미합니다. 그렇지 않으면 우리의 요구에 잘 맞습니다.
의존성이 커지는 것을 막기 위해 우리는 클라이언트에서 사용되는 npm 패키지에 대해 선택적이었습니다. Just 와 같은 라이브러리를 사용하는 모듈은 하나의 함수만 담당하고 종속성은 없습니다. 값 비싼 데이터 변환을 서버에 한정하여 Moment 와 같은 무거운 모듈 을 다운로드 할 필요가 없음을 발견했습니다. 종속성의 원인을 확인하기 위해 source-map-explorer 와 같은 도구를 많이 사용했습니다 .
m.uber의 사명은 모든 사람에게 어디에서나 쉽게 장치를 요청하고 장치 및 네트워크에서 추가 기능을 제공할 수있는 기능을 제공하는 것입니다.
window.performance API를 사용하여 첫 번째 상호 작용을 하는 시간을 감지하고 결과에 따라 상호작용하는 지도 환경을 숨기거나 로드합니다. 네트워크성능을 감지할 수없는 사용자의 설정 페이지에서 (상호작용하는) 지도를 켜고 끌 수 있습니다.
Preact ( React 와 같은 )는 변경이 발생하면 VDOM을 사용하여 새 마크 업을 생성하지만 이는 렌더링 호출 이 무료 라는 것을 의미하지 않습니다 . 아무 일도 일어나지 않을 것을 알기 위해 렌더링 을 위한 많은 JavaScript 잡담이 필요합니다. 우리는 shouldComponentUpdate를 광범위하게 사용 하여 렌더링 호출을 최소화합니다 .
서비스 작업자는 URL 요청을 차단하여 일반적으로 브라우저의 Cache API를 활용하는 사용자 정의 가져 오기 논리로 네트워크 및 로컬 디스크 가져 오기를 대체 할 수 있습니다 . 서비스 담당자는 JavaScript 번들뿐만 아니라 초기 HTML 응답을 캐싱하여 m.uber가 간헐적으로 네트워크가 손실되는 경우에도 컨텐츠를 계속 제공 할 수 있습니다.
서비스 작업자도로드 시간을 크게 줄일 수 있습니다. 디스크 I / O 성능은 운영 체제 및 장치에 따라 크게 다르며 대부분의 경우 디스크 캐시에서 데이터를 가져 오는 경우조차도 느려집니다 . 서비스 작업자가 지원되는 경우 HTML을 포함한 모든 다시 가져온 콘텐츠가 브라우저 캐시에서 직접 가져와 페이지를 즉시 다시로드 할 수 있습니다.
m.uber 클라이언트는 각 빌드 후에 새 서비스 작업자를 설치합니다. WebPack은 동적 번들 이름을 생성하기 때문에 빌드 프로세스는 새로운 이름을 서비스 작업자 모듈에 직접 작성합니다. 설치시 핵심 자바 스크립트 라이브러리를 캐싱 한 다음 HTML 및 보조 JavaScript 번들을 가져 오는 동안 지연 캐시합니다.
서비스 작업자가 너무 변동하는 응답 데이터를 캐시해야하는 경우 브라우저의 로컬 저장소에 저장합니다. m.uber는 몇 초마다 주행 상태를 폴링합니다. 라이더가 앱으로 돌아 왔을 때 로컬 저장 공간에 최신 상태 데이터를 유지하면 API 왕복을 기다리지 않고도 신속하게 페이지를 다시 렌더링 할 수 있습니다. 상태 데이터가 작고 저장된 데이터 크기가 한정되어 있으므로 스토리지 업데이트가 빠르고 안정적이며 궁극적으로 indexedDB 와 같은 비동기 로컬 스토리지 API를 사용할 필요가 없음을 알게되었습니다 .
스타일은 각 구성 요소 내에서 JavaScript 객체로 정의됩니다. 구성 요소가 렌더링되면 Styletron 은 이러한 정의에서 스타일 시트를 동적으로 생성합니다. 구성 요소로 스타일을 배치하면 쉽게 번들을 분할하고 스타일을 비동기 적으로로드 할 수 있습니다. 사용되지 않는 CSS는 절대로드되지 않습니다.
Styletron은 각 고유 한 규칙에 대한 원자 스타일 시트를 작성하여 스타일 선언을 중복 제거하여 최소한의 CSS 런타임 및 최상급 렌더링 성능을 제공 합니다. 우리는 m.uber의 모든 컴포넌트 레벨 CSS 생성에 Styletron을 사용합니다.
공간을 절약하기 위해 가능할 때마다 아이콘과 같은 이미지에 SVG 형식을 사용 하고 render 메소드 에서 인라인합니다 . 튜닝을 위해 수동 최적화와 함께 SVGO 를 사용하여 경로를 더욱 단축했습니다. 때로는 폴리선을 기본 모양으로 대체 할 수 있었고, 경로의 값 비싼 십진수를 피하기 위해 적절한 제수로 뷰 상자 치수를 사용했습니다.
전반적인 앱 크기에 대한이 전략의 영향은 중요합니다. 예를 들어 로고 크기를 7.4kB (png)에서 500 바이트 (튜닝 된 SVG)로 줄였습니다.
크기와 색상을 현명하게 사용하면 시각적 디자인을 크게 손상시키지 않고 사용자 정의 글꼴을 완전히 제거 할 수있었습니다.
Preact (like React) uses a VDOM to generate new markup when a change occurs, but that does not mean calling render is free. It takes a lot of JavaScript chatter for render to figure out that nothing needs to happen. We use shouldComponentUpdate extensively to minimize calls to render.
Service workers intercept URL requests, enabling network and local disk fetches to be replaced by custom fetch logic, which typically leverages the browser’s Cache API. By caching the initial HTML response as well as JavaScript bundles, service workers allow m.uber to continue to serve content in the event of intermittent network loss.
Service workers can also significantly decrease load times. Disk I/O performance varies greatly across operating systems and devices, and in many cases, even fetching data from disk cache is frustratingly slow. Where service workers are supported, all re-fetched content (including HTML) comes directly from the browser cache, enabling pages to reload immediately.
m.uber clients install a new service worker after each build. Since WebPack generates dynamic bundle names, our build process writes new names directly to the service worker module. On install, we cache our core JavaScript libraries then lazily cache HTML and ancillary JavaScript bundles as they are fetched.
Where we need to cache response data that is too volatile for service workers, we save it to the browser’s local storage. m.uber polls for the ride status every few seconds; keeping the latest status data in local storage means when a rider returns to the app, we can quickly re-render their page without waiting for a round trip to the API. Since our status data is small and the stored data size is finite, storage updates are fast and reliable, and we ultimately found that we did not need to use an asynchronous local storage API like indexedDB.
Styles are defined as JavaScript objects within each component. When a component is rendered, Styletron dynamically generates stylesheets from these definitions. Colocation of styles with components allows for easy bundle splitting and asynchronous loading of styles. CSS that is not used is never loaded.
Styletron de-duplicates style declarations by creating an atomic stylesheet for each unique rule, allowing for a minimal CSS runtime and best-in-class rendering performance. We use Styletron for all component-level CSS generation on m.uber.
To save on space, we use the SVG format for icon-like images whenever possible, and inline them in the render method. For tuning, we used SVGO together with manual optimizations to further shorten the paths. Sometimes, we were able to replace polylines with basic shapes, and we used view box dimensions with suitable divisors to avoid expensive decimals in paths.
The impact of this strategy on overall app size is significant; for example, we reduced our logo size from 7.4kB (png) to 500 bytes (tuned SVG).
With judicious use of size and color we found we were able to entirely eliminate custom fonts, without significantly compromising the visual design.
희박한 기술 스택이 항상 쉬운 오류 진단에 도움이되는 것은 아니기 때문에 다음과 같은 도움을주는 간단한 도구를 추가했습니다.
A lean tech stack is not always conducive to easy error diagnosis, so we added some lightweight tooling to help, for instance:
m.uber와의 공동 작업을 통해 우리는 성능 기준에 맞는 패키지에서 기본 앱과 유사한 경험을 만드는 데 많은 노력을 기울였지만 아직 완료되지 않았습니다. 개선의 기회는 여전히 많습니다. 다음 달에는 추가 최적화 계획을 발표 할 예정입니다.
또한 m.uber의 인프라 조각을 오픈 소스 아키텍처로 추상화하여 향후 Uber 웹 애플리케이션의 기반이 될 것입니다.
Through our work with m.uber, we have put a lot of effort into creating a native, app-like experience in a performant package, but we are not finished—there are still plenty of opportunities for improvement. In the coming months, we are planning on releasing additional optimizations, including:
Additionally, we are abstracting the infrastructure pieces from m.uber into an open source architecture which will serve as the foundation for future lightweight Uber web apps—stay tuned for an upcoming article on this topic.
2 Comments
[…] 프로그레시브 웹 앱 (PWA, Progressive Web Apps ) 도입사례(1) : Building m.uber : 세계 시장을위한 고성능 웹 응용 프로그램 엔지니어링https://www.aiforu.kr/%ed%94%84%eb%a1%9c%ea%b7%b8%eb%a0%88%ec%8b%9c%eb%b8%8c-%ec%9b%b9-%ec%95%b1-pw… […]
[…] […]