/ developer

Modernize the Frontend Stack at BCaster - An Experience

Never before the frontend world has observed such radical changes. We have a new JavaScript version every year supporting cleaner and more convenient syntaxes, and browsers are shipped with more powerful, performant APIs. The whole ecosystem is growing rapidly with new and exicting ideas/libraries/frameworks/tools introduced maybe every week. It is, however, also a challenge for frontend developers to keep a balance on following new trends, keeping themselves updated, and yet satisfying business constraints and requirements. Let me tell you a story about modernizing our frontend stack at BCaster.

Before continuing, please head to https://www.bcaster.com/livesearch if you are not familiar with our Live Map. It is basically a small web application where visitors can view public media in our platform within the current map view. Users can filter media based on type (video/image), the time posted, AI-assigned keywords or media quality. As the business grows, the Live Map becomes a vital tool to demonstrate our product to potential customers. It is required to be feature-rich, professional looking and performant.

The stage of legacy frontend stack

When I first joined BCaster a few months ago, our frontend stack was pretty much jQuery-oriented. That means we used jQuery to manipulate the DOM, send AJAX requests and rely on jQuery plugins to build some parts of UI. This approach is understandable as we were already using jQuery to work with Material CSS framework. However, as our team received new requirements to make the Live Map easier to embed in our customers' websites, it is better to prefer vanilla JavaScript and native browser APIs in order to send less code to consumers, thus speedind up the loading time and improving performance at the same time.

One other thing in our legacy code is that the whole code base is written in ES5 with lots of global functions/variables. This approach made the global scope become polluted and is widely considered a bad practice in JavaScript community. In long terms, it could potentially hide bugs and be a hindrance for our development.

The upgrade road

After closely examining the situation, we decided to set some goals for our upgrade process:

  • Code should be written in ES6: By using newer version of JavaScript we can write shorter code yet still guarantee the same behavior. By preferring let and const over var, we can eliminate the quirks of function scope. In theory, as our website is targeting evergreen browsers, we do not need to use Babel to transpile the source code. Yet it is still better to have an ES5 version to make sure the site still functions properly on at least IE11.
  • Minify and uglify all JavaScript, CSS assets: At that time, all of our frontend assets were sent to browser without any post-processing like minification and uglification. It means that end-users need to download bigger files and even though our Live Map was relatively small, it was not a good practice in long term.
  • Distribute built assets to a CDN: All assets should be put into CDN servers to improve load time for users from different locations across the world.
  • Improve developer experiences when working with frontend code: We would like to make developers' life easier by automating repetitive tasks and integrating tools that help team members to focus on writing good code.

Implementation

Integrate webpack, prettier and ESLint

The very first thing we chose to work on was to improve our developers' lives. It made sense because the sooner we could automate some tasks and introduce new tools to the team, the faster and easier it became to upgrade and improve the code quality. We added webpack into our tool chain for various reasons:

  • As a bundler, webpack can handle many different types of resources. We can process our JavaScript, CSS files, etc. by using just one tool.
  • It provides optimization tools to minify and uglify resources out of the box
  • Webpack has a development server where it watches for changes in source code and reloads browser accordingly. This is a huge save of time when developers do not need to switch back and forth between windows and manually refresh web page to apply changes. They can now focus on developing features.
  • It is easy to get started and we can configure to have advanced build process as our code base grows.
  • Webpack is quite mature and well adopted inside JavaScript community, so it is easier for our developers to find resources and ask questions if we get stuck with configurations.

The latest webpack version has adopted zero configuration philosophy, which helps us to spend little time to install and configure it to run. Initially our webpack config file could be this small:

module.exports = {
  mode: process.env.NODE_ENV,
  entry: {
    'map/index': resolve('src/map/index.js'),
    'map/styles': resolve('src/map/styles/index.css')
  },
  output: {
    filename: '[name].js',
    path: 'dist',
    publicPath: '/'
  },
  devtool: 'cheap-source-map'
};

As a result, we basically solved 2 of our goals with a minimal effort just by using webpack 🎉

Another tool we introduced to the team was prettier. We used prettier to auto-format our source files, resulting in uniformed coding style across developers. This can help to reduce the noise in git commits, where the diff reflects only feature rather than format changes. Plus it saves time as our developers can set their editor to run prettier automatically whenever a file is saved. No more time spent on manually aligning and beautifying code.

Lastly, we picked up ESLint with Google JavaScript Style guide to spot syntax errors early on and forcing us to follow best practices.

Upgrade to ES6 and improve code quality

As soon as we integrated ESLint and applied Google JavaScript Style guide, the whole code base became totally red as it used var to declare variables. So we knew we have to replace var with let/const. We would prefer const over let as it is advised in the coding guide and could help us to prevent variable unintentional reassignments. Also, coding with const is like a little challenge we can do for ourselves in order to come up with more declarative code than writing control statements.

Before starting the upgrading process, we set a motto: to make our app work with the new setup first, then slowly replace ES5 with ES6, while making sure everything is still working. This motto helped us to not overthink and deliver small, manageable and functional chunks of code. For example, in the old code base, it was trivial for other modules to use common functions as they were put into global scope. In order to remedy this global pollution, we use ES6 modules to put those functions into a helper file, and import them as needed.

For global variables, we put them into window so at the beginning they were still accessible from different modules.

// Before
var map;
function onload() {
  map = new LeafletMap();
}

// After
window.map = null;
function onload() {
  window.map = new LeafletMap();
}

window also served as a signal for us to know which variable was global. Then we moved them into function parameters and passed them explicitly.

// In other modules
// Before
function addMarker(marker) {
  window.map.addMarker(marker);
}

// After
function addMarker(map, marker) {
  map.addMarker(marker);
}

// Then in modules that use addMarker()
addMaker(window.map, maker);

This could make some functions long as they receive many parameters, and sometimes they were ambiguous as parameters were forced to pass in correct order, resulting in e.g. this funny function call:

initMap(map, 65.1111, 23.22222, true, true, null, null, 'id-main-map');

This was the time for ES6 to shine. We converted those functions to take only one object as their parameter and used object destructuring to retrieve required values. By doing this, we made clear the meaning of each value, it allowed us to skip optional data and to introduce new values at ease.

function initMap({
  map,
  lat,
  lng,
  shouldCenterMap,
  autoUpdate,
  defaultMakers: [],
  mapId
}) {
  // Doing logic here
}

initMap({
  map,
  lat: 65.1111,
  lng: 23.22222,
  shouldCenterMap: true,
  autoUpdate: true,
  mapId: 'id-main-map'
});

By applying this principle, we finally removed all global variables and made our code clearer, bug-free from global pollution.


We are evil global variables. We are everywhere. Treat us well and we do no harm.
Photo by rawpixel / Unsplash

Side note:
During the time we used window to share variables among modules, there was one interesting and funny bug. Please look at the code below:

<div id="map"><div>

// map.js
getUserLocation()
.then(data => initMap(window.map, data));

// module.js
window.map != null && window.map.getCurrentCoord();

We got:

TypeError: window.map.getCurrentCoord is not a function

Can you spot the bug? It is weird because that method is only available after window.map has gone through initMap() and we did check if the map has been initialized before trying to call getCurrentCoord(). The reason is quite amusing: when the page had first loaded, window.map never was null or undefined because IDs are global, and we happened to have an element with ID map there. And module.js for some reasons run before the map was initialized, thus resulting in the error above.

So the lessons for this are:

  1. Always prefix ID of DOM elements if their purpose is to be used with JavaScript, e.g. id="js-map".
  2. Global variables are bad, bad, bad.

Distribute assets to a CDN

Lastly, we wanted to put our frontend resources to a CDN so our users from different parts of the world can still load our website fast. We chose Amazon CloudFront as our CDN service provider. It was pretty straightforward as we just needed to upload our resources into an assigned Amazon S3 bucket, then they would be automatically available in all CloudFront's points of presence (PoPs). The challenge was: what is the workflow and how could our developers publish resources at ease?

To solve this, we decided to follow semver to version our resources. This helps to burst the cache of CloudFront's PoPs when we release a new version in a manageable fashion. To facilitate our developers' workflow, we built a small CLI script that automatically increases release's version (either major, minor or patch), builds resources using webpack and pushes newly built artifacts to S3. It is worth noting that, depending on different deployment servers, we will tweak webpack config on the fly to have different results. For example, resources published for our dev server remain untouched with no minification/uglification to make debugging easier.

Results

So far we are pretty satisfied with the outcome. The whole code base was converted to ES6, providing a stronger foundation for us to develop new features at ease and to make things more future-proof. We managed to split our live map into smaller modules and each of them will be imported dynamically after the entry file is loaded, and did we tell you our main file is now under 100 kB?

The development workflow was improved with automation and better tooling. We hope this will make our developers more productive and ready for upcoming requirements.

What's next?

Obviously there is still lots of room for improvement. We are planning to let our customers include our live map into their websites/apps by including just a single snippet. Therefore it is critical to keep the bundle sizes small, dependency-free and performant. We could try to remove jQuery and prefer smaller libraries/native APIs to build our application. But that is another story. Stay tuned! 😉