The front-end in software development is evolving at such a rapid pace, and it can be hard to keep up. But the key is to adapt slowly and ease into new changes, which can make our developers’ and users’ life easy. Here’s a detailed breakdown of the stack choices we made (and why) along with the different pieces in our front-end.
A bit of front-end history
We started with a PHP based application using jQuery in the front-end, and soon we learned we need an architecture on the front-end. So we chose Backbone in 2012. It was very lightweight framework providing us with an MVC architecture and move closer toward the existing codebase. We were also excited about all the benefits of having single page application. The transition from jQuery based app to Backbone was easy and fast. We decided to use CoffeeScript over JavaScript at that time for the syntactic sugar it provides. We are using Sprocket for Rails asset pipeline which handles JS and CSS bundling.
We built all our app with Backbone. But as Backbone provides a lot of flexibility and we did not have a strict guideline on front-end development, the code base went messy and hard to maintain. Also after a particular point in time, it was getting hard to optimize the apps for performance due to the limitations of our front-end infrastructure.
We were not going anywhere farther with that infra, so we needed a big refactor. We reviewed different frameworks and libraries and decided to go with React ecosystem for three main reasons.
- Server-side rendering (This will increase the perceived experience for a user)
- Rendering performance (Virtual DOM diffing and minimal updates on dom will definitely boost the rendering)
- Maintainability (Component-based declarative and unidirectional approach will make the app more predictable and maintainable.)
We had 2 options for migrating backbone to react
- Start migrating Backbone views to React component and later update the model to Redux store.
- Route by routes, migrate everything on that page to React.
We went ahead with the 2nd approach as migrating everything to React would take a while, and we didn’t want to reduce the experience for our user even more loading React and Backbone together. Also, the first approach would not have helped us with server-side rendering.
Current State
Currently, the major flows have been migrated to React. However, there are some pages which are in Backbone so we do have to manage CoffeeScript + Backbone and React Ecosystem. But all new development happens on React.
Our New Frontend stack has following pieces.
Language
ES6+: We moved out of CoffeeScript as we no longer need syntactic sugar it provides. Also, the React community and documentation is built around es6, so it was the best choice for us. We are using Babel to transpile it down to es5. We are planning to move to babel-preset-env to create smaller bundles.
Scss: We are using ‘libsass’ for sass compiler, and ‘postcss’ and ‘cssnano’ for CSS optimization.
EJS: We also use EJS on the server side as templating engine for things which are outside of React scope. That’s basically the page skeleton.
Isomorphic application
To improve the perceived performance we opted for having Isomorphic Application. 95% of React code is shared across server and client, and there are only top level wrappers which have client /server specific code.
The first page renders from the server with meaningful content for that page and then all subsequent navigation is handled on the client side as SPA (single page application). Our routers have the responsibility to load the critical data required for that route and non-critical or non-visible data is loaded after component mounts or on a user action.
Client-only render
Along with the isomorphic application, we also have a way to load the complete app on the client side. In case the node server is down or we want to reduce the load on the node server, we serve the client-only version. In client-only render application boots on the client.
A/B Test Infra and usage tracking
Our whole development is data-driven. We launch any new feature in A/B test, learn from the usage metrics and improve the experience. We have our own inbuilt tracking and A/B test infra.
Components, App State and Data Flow
We define components based on their responsibility, and hence, it’s not merely smart and dumb components, but they can be defined as smart enough to handle responsibility and dumb about the outside world (state).
In our application models, UI state, application states can be termed as ‘states’. There are two entities which manage these states.
React components usually manage the UI level (like in progress state, open state, etc.) and temporary states (like form inputs). We decide what state a component should have based on its responsibility. Temporary states are moved to Redux state once they are submitted or required by other parts of the application.
Redux Store acts as a modal for our application and all the persistent (persistent in js memory), and app level data are kept inside Redux store. We normalize our Redux store (reducers) to maintain a single source of truth for a data.
For any component, data can come only either from the parent as props or through the connected state as props. There is some data which needs to pass from top level to any level which we do through context, but we build higher order components to pass context data to the component as props similar to React-Redux connect method.
Testing
End to End testing: Currently e2e test is written in Selenium, which has to pass before we push to prod. We might evaluate puppeteer in future, but as sometimes we have to test across browser we are sticking with selenium.
Unit Test: We use Karma as test runner, Mocha for writing a test suite, Chai as assertion library, Sinon for stubbing and Enzyme for writing react component test. Before we were writing the test for only shared utils and UIKit components. But recently we are being stricter about test cases, and every PR must have 75%+ coverage. We use Istanbul for code coverage.
Build Tool
We have two different build systems one for building Rails/Backbone assets and one for building React. In the end, we combine the assets, source maps and manifest from both the build system.
The build tools used for building React assets have been configured to enable better developer experience and maintaining code quality.
We use webpack for bundling js and CSS which currently have three different configurations, one for development, one for production and one for the test server.
On the production configuration, we have used several plugins/modules for optimization and quality control like HappyPack, BundleAnalyzer, CommonsChunkPlugin, UglifyJsPlugin, etc.
We also use (HMR) Hot Module Replacement for the development, and currently writing a wrapper for reliable hot reloading and giving feedback on the page while hot reload is happening.
We also have built some custom loader to get some of our product specific behavior with the webpack.
On top of Webpack, we use gulp for handling build tasks which are outside of webpack scope, like font icon generation, image optimization, mixing Rails and React assets, etc.
Code Quality
Apart from having proper review cycles, we have added a few tools to maintain code quality. We use ESlint for js linting, Flow for static type checking and CssComb for SCSS formatting. We also have set these linters on the pre-commit hook.
Apart from linters, we use code climate to report quality and coverage for each PR and we use Travis for running the unit test case.
Performance
We are trying to follow PRPL pattern for better performance on our site. Our CDNs are configured for http2, and we try to get maximum benefits from it.
We do code splitting to serve the first page faster. Right now it’s majorly route based splitting with some splits to lazy load few libraries, but we are working on it aggressively and trying to find the best strategy for splitting.
We also try to filter things which are not required for initial page rendering, for example on the first page-request we only send the manifest information of files which are used on that page (bundles included on that page) which would otherwise require extra 110kb of gzipped data to be sent to the first-page load.
We also use preload, prefetch tags for an early load of those assets. To reduce the code repetition for including preload, prefetch, and filtering we have build plugin hrImport which adds the required code for this task.
UIKit
We want to go toward atomic design principal, and we started this with creating building blocks (components) for our application. This is currently in the early stage and contains basic elements like Buttons, Pagination, Modals, Popups etc, and also the design guide. But the goal is to get this on a stage where designers can create a composite design for a page or feature using those components.
This also has auto-generated docs and playground for those components. At some point in time, we will also open source it and make it publicly accessible.
New changes in the near future
- We have planned out a performance checklist for 2018 which will be measured by TTI (time to interact) and perceived performance.
- Optimize the build system which will improve the site performance and developer experience.
- We are rethinking about the mobile use case for our product and make application mobile friendly.
- We have also started complete site redesign to improve the user experience.
- We are working on UIkit to bring it to a stable state where most of the design elements can reuse the UIKit Components.
- Improve the overall coverage by imposing the unit test on new features and bug fixes.
By the way…we’re hiring! If you’re interested in helping us build the future of engineering hiring, apply here.
Sudhanshu works as a part of the engineering team in HackerRank managing Front-end Infrastructure. He loves building anything with JavaScript and explore patterns and tools around it.
Leave a Reply