Engineering
Fat, Static, and Happy
How to build an app without spinning up a server - and why.
Bloomberg Lens is a Chrome Extension and iOS Share Sheet application that presents related news, company data, and person information for any article on the web. We (Postlight) built Bloomberg Lens with Bloomberg early in 2017 using React. As a third party partner, we wanted to pose as little burden as possible to their existing engineering teams. We managed to build Bloomberg Lens without requiring Bloomberg to deploy any new APIs (although we did have to ask them to turn on CORS).
Can It Be Static?
The first question I ask myself when architecting a new application is: can it be static? By static, I mean no dynamic server between the user and the initial application load. Ideally, this also means no bespoke APIs. Nothing that can overflow with log files, get hacked, or become sluggish. The app should either work perfectly or be completely down.
When building Bloomberg Lens, we encountered many challenges that we thought might require a server. However, we succeeded, and the production app only uses pre-existing APIs.
Why Avoid APIs and Dynamic Servers?
We’ve built numerous applications at Postlight and we’ve maintained them for years. The applications that take the least energy to maintain are those that are static and immutable. We’re always building new products, and that means we don’t want to spend time and resources maintaining existing ones. Once we launch a new app, we want it to be as stable and run as maintenance-free as possible.
We launched Bloomberg Lens to production, made a handful of bug fixes over the next 24 hours, and haven’t touched it since. Zero downtime. It’s been humming along with no issues.
Things We’re Doing Without a Server
- Converting the chaos of HTML news articles into the content text for extraction. We’re using Mercury Parser to accomplish this feat.
- Detecting potential person names to match against their entries in Wikidata. Accomplished using Compromise, the incredible JavaScript natural language library that runs in the browser.
- Querying Wikidata and Wikipedia from the user’s browser, and ensuring the minimal number of network requests by using redux-query.
- Rejecting outlier person results using client-side logic (more on this in the “Extractor” section below).
- Scanning for company names, based on a giant list of potential company names and some complex regular expressions (more on this in the “Extractor” section below).
How We Built Bloomberg Lens
Bloomberg Lens runs as a Chrome Extension and an iOS Share Sheet Extension. We built a single React app that runs entirely in isolation, only receiving data via the URL. We provide environment-specific hooks using React’s context feature. We pass around the big ball of React code without issue, since it’s a static and isolated app that knows nothing about its surroundings.
Shared React App
The foundation for Bloomberg Lens is a shared React application. A small harness exists within the repository to run the application standalone, as a solitary web application. We did all of the app development in a typical hot-reloaded and URL-based fashion. An incredible win given how hard it is to iterate on a Chrome Extension or iOS Share Sheet application.
We share the built version of the application as an artifact committed to the same repository. Other repositories reference the React app at a specific version. This means that we can deploy an update to only the Chrome Extension or Share Sheet app if desired. Having this flexibility is important to ensure we can release only the features and bug fixes we want when we want. We don’t want to be forced to deploy a new version of the React application if we only need to make a change to the Chrome icon, for example.
Extractor
The logic that scans articles for company names and people was extracted into a separate repository. Having a standalone repository allowed us to easily test it in isolation at the command-line. When we made upgrades to person detection, e.g. adding rare first names like “Elon”, we were able to see if it affected any other name detections in unexpected ways.
Person names are detected using Compromise, and then likely false positives are filtered out. After Lens is invoked on iOS or Chrome, we use Wikidata’s API to search for the detected names. We pass the Wikidata IDs to the React app in the URL. We query Wikidata for the full information for each person, then filter the results based on many criteria, which includes removing people who are more than 100 years old.
Company detection went through many iterations. We ended up relying on a multistep sanitization process, removing suffixes like Inc. and LLC. However, we had to be careful to not end up with names that were too easy to match, like “Team” or “Next”. The normalization process also allows for human-defined company name overrides (using a TOML file) and aliases to ensure detection works really well for common companies.
Chrome Extension
The Chrome Extension is a small wrapper application that handles mounting the actual app inside an iframe. We run detection for companies when the page loads, but we don’t start the actual application until the user clicks the icon in the extension bar. The React application is bundled inside the Chrome Extension as a minified JavaScript payload.
iOS Share Sheet Extension
We added a Share Sheet Extension to Bloomberg for iOS. The extension handles passing the data about the page being scanned (the HTML or the URL, depending on how it was invoked) to the React application. The React application is loaded from a static remote server so it can be updated without a new App Store submission.
iOS Web App
The actual web app that runs inside the iOS Share Sheet Extension is deployed to a dedicated URL. It exposes a few global JavaScript functions that the Swift code calls with information about the page being scanned (title and body HTML or URL). It also handles passing the message to the extension when the user taps the close button, because the close button is implemented in JavaScript.
How Fat?
Our JavaScript payload is 800KB, gzipped. However, that means we don’t have to hit any live APIs for that information. We optimized the loading of the application and were impressed with the performance even with payload of this size. Our CSS is also entirely contained within the JavaScript payload. The Chrome extension bundles the entire React app and Extractor, so we don’t have any network cost. And that makes everyone happy.
Jeremy Mack is a Partner and Director of Engineering at Postlight. Can your next app be fat, static, and happy? Drop us a note: hello@postlight.com.
Story published on Jun 8, 2017.