How we created a go-to destination for the “Cricket World Cup”- the most celebrated event in Indian Sports!
We all know what happens when a team starts with a new project, especially if it is of web front-end — ideas pour in from all direction, what framework to use, what architecture to use, how to render content (server-side or client-side rendering).
Considering how the ICC CWC was only two weeks away, we needed to quickly prototype, develop, test and iterate. All of these pose a significant challenge and here is the story of how we achieved it all.
Finalizing the Tech Stack: After some discussion on our requirements and time frame, we decided to have client-side rendering with React on the front-end and Django on the back-end. This was mainly because of a couple of reasons:
- Our team is proficient in using Django and we can scale very confidently.
- Some of us had worked on React earlier, which meant that we could recover faster from our experimental mistakes.
- With React, we get CRA which means that we can bootstrap very fast and set up a basic PWA, which was very important for us as most of our traffic was from mobile.
We used Webpack as the build system and Babel as the Javascript compiler. Babel converts the code written in es6 standard into backward compatible Js code for older browsers. This was critical as it helped in targeting that entire category of users who were enthusiastic about cricket but were on older Android devices with an archaic WebView.
Architecture: Choosing the architecture with React is easy as Flux is the universal choice. Flux along with React makes it easy for us to reuse components which, in hindsight, was very necessary given that we ended up writing 100s of components.
The decision to use client-side-rendering was rewarding as it enabled our front-end and back-end engineers to work in parallel and have less dependency on each other for integration. This way the server-side engineers were able to focus on scaling and creating APIs which front-end engineers ended up consuming, while we (part of the front-end team :) ) were focusing on user interaction and client-side optimization.
As we had multiple screens in the app showing live scores, with each being updated in real-time, we had to use a global state so that no bandwidth was wasted (We all have JIO but the state of 4G in India is still a mess). Came Redux to the rescue. With Redux:
- We could maintain the global state for our application, cache the data and prevent the redundant API calls (giving the server-end team some breathing space). All the screens which needed to show scores could then subscribe to the store and get the latest score data from one place.
- As our app was offline enabled, with Redux store, we were able to easily save and maintain the most recent and useful app data, so the user can navigate the app even when he/she is offline or on a slow flaky network.
With the help of the Service Worker API, we were instantaneously showing the most recent/saved data and updating the content later asynchronously. That is how we were able to serve our low-end devices’ users and provide an experience similar to that of a native Android application.
This architecture provides multiple layers of caching across our tech stack, 4 to be precise, back-end and front-end combined.
Performance: As most of our users are on mobile, low to mid-end mobile phones, we need to take special care of performance for the web page. Some of the steps we took are:
- Code Splitting: As the number of components increase, the size of Javascript bundle also increases. After a certain point in time, it becomes a very expensive process for the browser to fetch complete bundles and then parse it. To counter this we started splitting code and load the critical code first and then the remaining code asynchronously. We used Webpack’s async import to achieve this. There are other libraries present to achieve this purpose like react-loadable. Using code-splitting, we decreased the size of the initial Javascript bundle to be downloaded initially by over 200 KB.
- Lazy Loading: Although Code splitting is also one form of lazy loading, I’ll also mention the other resources we lazy-loaded:
- Images: We used the browser’s interaction-observer APIs to lazy load our image.
- Libraries: We come across many code-bases where index.html is filled with links to Javascript and CSS libraries are added in <head>. As these are render-blocking resources, this adds significant time to the startup time of app. We integrated most of our libraries with npm, which helped us to leverage code-splitting to lazy load.
- Fonts: We used Google’s web-font-loader to lazy load our fonts. This saved us around 20 KB worth of render-blocking render time, which is a lot, considering we have only seconds to retain our users.
Lazy Load and Caching are the rules of thumb for performance, if you can get it right : 10x dev.
In the end, performance is very subjective and it depends what developers are trying to achieve and metrics vary substantially. For starting, developers can use Google’s guide to optimize your web page. Below is the screenshot of the Lighthouse audit report. The performance is fairly good considering the short time we had and the fact we had to load some compulsory render-blocking business-related libraries during startup. This shows that even under constraints, we can achieve fairly good performance by following some standard good-practices and keeping in mind the performance cost of the code we write. Not to mention the coffee, the pressure to go live and a lot of internal motivation.
Well, this was a busy couple of weeks, but totally worth it, as we were able to serve around 2 million users across the thrilling world cup.