Myntra’s PWA Architecture

Vijay Krishna
9 min readSep 10, 2018

--

We believe that building a good maintainable stack needs a right blend of right technology choices and good developer discipline. We went through guidelines content created by google, and many medium posts on PWA. We also created our own internal guidelines too.

Myntra’s new Progressive Web App

We also had our own challenges which were different from other companies. We needed a PWA stack — meant to scale, where hundreds of developers build new features and release them every day.

As mentioned earlier, in order to make the code maintenance easy and speedy development, we had built multiple micro apps, mainly the following 3:

Home & Search — App which contains home, search, product, login pages

My Account — App which contains pages such as profile, orders, etc.

Checkout — App which contains the entire cart and payment flow.

Myntra PWA Architecture

App Template : This gives a skeleton to pwa app with myntra header, navigation and common features on node such as authentication layer etc.

Engineers just fork this code and get PWA.

The app template itself was built on following tech sack

  1. React / Preact
  2. React-router
  3. Nodejs / Express with dotjs for html based templating

We carefully chose our tech stack optimizing for file size (thus also speeding up execution) We didn’t use redux, redux-saga due to their size vs benefits they provide to our use-case and maintaining a state by ourselves was not that difficult problem for us. The interaction between sibling components were minimal. This also helped in keeping stack lightweight. We were also cautious in using react features, which were not available in preact.

Please note preact may not be a good thing for every app which uses react. react especially react 16, has a higher rendering performance when an app has highly complicated huge component chain with frequent UI changes. Since we don’t have any of that, it made sense for us to use preact.

This architecture also needed following components:

Smart Shareable App Shell

We are fans of google’s PRPL pattern which has a great emphasis on the performance of app delivery and launch. So pushing critical resources to browser was our first priority.

We needed a shell with faster first paint. Rendering it with any javascript library meant it would delay the first paint. Also, as shell is shared across the apps, we kept it independent of specific library.

With this, each app team can just focus on their content and not really worry about shell features such as sidebar ( we do have dynamically configurable navigation system) or in app notification alerts (which still needs to be shown even if the user switches pages. So it needs to be outside react router)

Every app needs to interact with the header and control elements on header.

Ex: add to cart action should increase count on header. So we built a simple command center which is globally available called as SHELL.

All the shell related features are built inside SHELL.

Ex:

SHELL.setCount(‘cart’,’10’)SHELL.notify(“Item added to bag successfully)

Shell also contains base inline css styles not to affect critical rendering path.These are common styles such as default button styles, font sizes, font icons etc. Even the critical js functionalities are inlined. ( Size was less than 3 kb)

Shell proved to be even more useful eventually; we started putting all common and important features such as google analytics into this including general events such as button, link clicks.

Ultimately, shell was a single file with all critical css and js inlined. We published it as a module to our internal repo to make maintenance easy.

Though the shell is a single file, we do have a simple code organization with separate css and js files which would be combined and minified to single dot template file through build process.

Smarter Component library:

This has multiple shareable components like image, button, box, loading, tabs, carousels etc.We enforced a development discipline to make these components similar to default html components with the same attributes to ensure a zero learning curve. These are components which can do lot more than default html components, they are smarter and context driven.

Let’s talk about one of our star component.

Smart Image component

Guideline: Use our <Image> component instead of <img>.

Our site is mainly image driven. So optimising them was very important.

Image component does all the things that normal img tag does with a lot of engineering smartness built into it.

Lazy Load

Image component has the logic to lazy load — It loads the image only if it is visible in the layout ( with some tolerance to outside viewport area to preload next image)

Built in animation

When image appears/disappears in viewport, there is a smooth fade-in/fade-out animation

Smart image format selection ( webp/jpeg)

Picture tag is used in the component to load webp image on all supported browsers. jpeg is a fallback for other browsers.

Dynamic image quality control

This is very interesting. We use cloudinary to store and consume all our images. Cloudinary has a url pattern to specify quality, width, height and also type of the image.

Ex: https://res.cloudinary.com/demo/w_300,h_200,q_60/sample.jpg

Our Image component controls these parameters in a smart way to get the optimal set of images.

For example, we use Network Information API to identify user network type and load lesser quality image for cellular and higher quality image for wifi!

The developer need not worry about these optimisations when he uses <Image> component! How about that!

All the other components are also built in a similar way !!!!

Shareable APP Template

After an App Shell and Component Library, our focus moved on to setting up a base for server.

We wanted to have an

  1. Isomorphic library to make ajax calls.
  2. Middleware Set — ex: Mechanism to authenticate all requests.
  3. Good logging mechanism.- we used winston.

Smart Agent — Modified Superagent

We chose superagent along with superagent-cache library which enables caching on node as well as on server. We patched superagent with our own functions to handle some common errors, configurable url setup and common logging, just like our smart Image component.

Guideline: Use SmartAgent to make any ajax calls on both server or client.

Smart agent by default knows myntra API formats. It knows how to handle different types of errors.

Ex: If a server throws 500 error, it redirects users to “Something went wrong” via react router by default.( not even a page reload).

We made sure developers have granular control incase they want to override. The syntax of smart agent is exactly same as superagent, with no learning curve

Middleware set

We built a set of middlewares ( we already had many middlewares we just added few more to the collection)

Smarter Logging Mechanism

Used winston in a dev friendly way but with a twist.

Usually when you use a logging library devs will create their own variable. But devs have a habit of using console.log() everywhere. There was a chance that devs might miss to use winston and use console directly.

So we patched default console and made winston object as console! So all console.log is now handled by winston.

Small change but huge impact!

Building Main Application

Once we have all the pieces of puzzle ,so called modules, ready, it was easy to put them all together into an APP. We are using node 8.4 with npm 5.3. No Yarn as npm does a decent job now and it has package-lock.

However, we had one more major task — delivering the javascript,css, assets efficiently.

Build System

Webpack and Bundle Splitting

We use Webpack 3 with babel. We use route based bundle splitting. We use all possible compression to minimise the content.

We also integrated webpack-bundle-analyzer within build system to keep an eye on size and looked for opportunities whenever possible. Rather than fixing file sizes at the end of development phase , we fixed the size issues immediately after adding each module to ensure there is no size bloat.

JS Bundle Size

Our previous bundle was around 350kb, whereas today it is ~105kb (our main bundle is around 65 kb gzipped and all other pages at less than 20kb) and we believe we can reduce this further.

CSS Bundle Size

Once we built the system, we realised that our css files for all 8 pages put together was around 6kb gzipped. We had another 4kb core css which contains basic text, button, theme styles in the app shell itself.

By writing a nice base css styles in the right way( basically followed bootstrap pattern but used only the ones we needed) we actually reduced css bundle from 33kb gzipped previously to 10kb gzipped!

Usually css files are made as separate files per page. In our case since it was a smaller in size, we decided to inline it in the shell during build. This would avoid a network call and also critical rendering path is not blocked.

This may not be ideal for every company. As any minor css change would lead to full page change. But it works for us as we have weekly release cycles even our page contents gets updated during these cycles.

Service Worker

We use sw-toolbox. We have implemented different cache buckets based on type of the resource like images vs api data. And we believe we can further improve in next releases by writing custom handlers. We will upgrade to workbox in the coming days.

Dealing with Browsers without Service Worker

We had two mains browsers which myntra had to support as there is a good amount of traffic from them i.e, Safari and UC browser.

As we were aware of this from the beginning, we considered how to optimize for these browsers.

Local Storage as alternative to Service Worker Caching

  1. Our SmartAgent Module handles its own caching on the browser side with LocalStorage. If the browser doesn’t even support LS then we fall back to in-memory.
  2. Though it’s not a match to service worker it really helps in at least handling the majority of api data calls. Also our PWA is a single page application, so switching between the pages doesn’t really reload.
  3. And since it’s already coded in our SmartAgent we don’t really need to do anything extra! However we can’t do offline support yet.
  4. We did add some correct apple specific meta tags to show the home icon on iphone.
  5. Also added back button behaviour as default option near burger menu while browsing pages; to augment not having back button in iphone in standalone “app mode”

UC browser

  1. While UC browser might bring Service worker soon till then our LS cache should help.
  2. UC browser does not work with webpack bundle splitting and debugging it is very difficult. Usually we had issues due to unsupported browser/javascript features, And including a set of polyfills solved our problem.

Going Live

We did a quick load test on production in early September. Then we slowly moved 50% traffic and now we are at 100% traffic served via PWA.

Future and Areas Of improvements

We still have lot to do. We will keep on venturing new things!

Animations

We are inspired from FLIP animation techniques. We would like to build animations using window.requestAnimationFrame(). And will be targeting 60fps. This will be one of our priorities.

We will also build other pieces of Myntra Mobile Web experience such as My Profile and Checkout as PWA.

Better Caching Technique in service workers.

We need to write some custom caching strategies to handle few cases.

Desktop site as PWA

We will start working on our desktop site very soon. Our architecture supports desktop apps as well. So getting desktop PWA should be quicker

Better Notification Handling

Give more granular control to the user in terms of type of notification he would like to receive. Also start preloading the links in the notification.

Better offline capabilities — using background sync — User can place an order if offline too, from the browseable products available offline and once he/she is online the actions are synced.

Predictive Preloading

Use the analytics to predict user clicks and preload them. We are inspired from google AMP preloads!

For example, most of the users click first two banners on the home page so we can actually pre load them . In fact we can do better prediction for the logged in user.

Reducing bundle size even further

We are planning to build browser specific bundles to reduce sizes even further. Very useful if polyfils are being used. For chrome probably we don’t need polyfils where as for old browsers we would need. By creating separate bundles for these use-cases will reduce bundle size as well as improve speed of execution.

More Integration with Google Ecosystem

We are planning to experiment with google payment api.

We will keep you posted with further implementation and performance details in the upcoming blogs.

Thanks for reading our journey! Hope you find this useful.

--

--