How we built chobani.com

Serverless, PWA, React and other fun stuff

TLTR

  • Full static pre-render: Serverless using S3 buckets + Cloudfront
  • Render: Universal React Application
  • Data sources: Contentful (headless CMS) + Jobvite API (jobs management)
  • Dynamic content (search, related items…): Elastic search
  • PWA 💯
  • Lazyload ++
  • A note about Accessibility
  • A lot of love

After months of hard and exciting work, which started in April 2017, we can proudly share our latest production: https://www.chobani.com/

For those who don’t know (basically non-Americans), Chobani is the #1 yogurt company in the US. (My favorite is the Smooth Strawberry, which I mix with chocolate chips. Deliiiish).

Me mixing chocolate chips in my smooth yogurt. Also me using our awesome stack.

Init

The previous website was built using Wordpress. There’s nothing wrong with that, but the Chobani team wanted a more modern stack in addition to more functionality and flexibility. They needed the ability to create modular pages with a wide set of configurations, manage assets, and add products, recipes or articles.

A Headless CMS

Given our success using Contentful for our own site, we opted to migrate chobani.com over to it as their new CMS. Contentful is a CMS that operates separately from the frontend architecture (hence the term “headless CMS”). It allows teams to generate custom content models and define a structure to fit their needs, rather than being forced into a predefined CMS structure.

Headless CMS are very convenient. They allow you to centralize content, which can then be utilized across different products. It’s the first step of a serverless architecture model — detaching the content from any specific language or framework, therefore giving more flexibility to developers (but also more work, as we have to fully handle the render).

We also included Jobvite as a different data source. We chose their API because we wanted something more customizable and optimized than what an iframe solution offers.

Universal React App

Obviously. There’s no point presenting React here, but it was clear that building a Universal Application using a component oriented framework was a solid solution. Server-side rendering is also a standard every website should have.

We gained experience working with React over the past few years, and for this project we pushed our previous starter further to be up to the challenge:

  • Webpack 3 and a set of handful plugins/configuration: Babel, Post CSS + CSS modules, Hot reloading…
  • Server-side rendering: Node JS + Express
  • Redux + Immutable + Sagas + Reselect for data
  • React v.15 (v.16 wasn’t released by the time), React router v.4

A full static website

Rendering using React is cool, and with SSR (Server Side Rendering) it’s even better. But Node JS servers are single threaded, and they struggle to render a page as it transpiles JS using Babel on runtime as well as generates CSS classes using Post CSS. Starting with a number of request per second (~14), adding clusters and caches helped a lot but it wasn’t very efficient, as the number of request per second was still pretty low (~200).

We decided to take a step further and build the final website on a static S3 bucket, using Cloudfront as a CDN.

The Serverless approach is not new, but with headless CMS becoming more and more available, such as Contentful, it is becoming a popular approach in the development process. The idea is to generate the HTML of every single page of your website and put them in a static bucket with a CDN on top for an optimal delivery. There is no server to go through to generate your page — it’s already physically there.

If you want dynamic data, the static front-end can use external APIs/cloud functions to fetch data and then inject the appropriate HTML.

The Serverless architecture is obviously optimal for performance reasons, as well as SEO and accessibility.

Building the data…

So far, our deployment workflow looks like this:

  • We fetch JSON data from the different APIs exposed by Contentful and Jobvite.
  • The universal React application renders the HTML using the data.
  • A cloud function fetches the rendered HTML of every single available page to dump them in a S3 bucket.
  • Independently, when we build the application, we also deploy the generated static files (JS, CSS, fonts, etc.) on the same S3 bucket.

Cool! But we wanted to go even further, we wanted to optimize even more.

Fetching external APIs is great, but in a production context, having thousand of users per day using APIs with a unique public key could lead to slow or even cancelled requests, ultimately costing the client more. For instance, depending of the plan you choose for Contentful, there is a limited number of requests per day you can make to the API.

GraphQL emerged a few years ago and allows developers to access data in a unique and seamless way, whatever the data source is. Contentful is GraphQL ready, but not all products have a GraphQL server ready to be used, exposing a classic REST API instead.

Finally, for any kind of project, we always have the same problem: dealing with data. We don’t want to directly talk with APIs and endpoints, but we still need to parse and treat some data.

For optimization reasons, we usually have back-end developers building a middleware to generate static JSON files adapted to our needs, depending on the application and the design.
 
That works great, but involves more people, a lot of back and forth, and we almost have to rebuild that middleware from scratch depending of the server we are on and the data source we are dealing with.

We got tired of that. We thought there must be a better solution. We wanted something flexible, that any front-end developer would be able to use without bothering a back-end developer all the time. Something independent of the project, independent of the data source. Something optimized and elegant that we build once and can re-use on other projects.

The goal: take any type of data source as an input, have an interface for developers to request and format exactly the data we need, and ultimately build the JSON files.

… Hello, Static Data!

There we go! We built a tool to abstract this process, which is one we always end up making over and over again.

In a nutshell: It’s a tool for developers to easily generate static content (JSON files) from any type of data sources, using GraphQL queries, on a simple interface. It can be used for any type of projects and contexts, completely language/framework agnostic. Some examples:

Data sources: Google Drive spreadsheet, Wordpress, Contentful, Instagram API…
Type of projects: Websites, mobile apps, installations, banners…

As a developer/user, we don’t have to worry about the data source. We write our GraphQL queries, give them a name, and automatically generates the static JSON files on a bucket (AWS/GCP compatible) after either a webhook is called by the data source, or a periodic check.

That is a game changer for us!

Search, related items, and other dynamic data

Static data are great, but how do you deal with dynamic data?

We decided to use Elastic Search. It’s a distributed, RESTful search and analytics engine capable of solving a growing number of use cases. It’s a very elaborate, quite complicated sometimes, but really powerful service.

Every time a specific entry is updated in Contentful (in our case, a product, recipe or press entry), our static data tool updates the index of our Elastic Search server, and then generates the static content.

Therefore, the front-end has a unique endpoint accessible to request items depending on specific parameters (such as a search term), retrieve related products based on the current one, list top recipes, and other needs.

PWA 💯

2017 was the year of PWA. We wanted to embrace it for Chobani because it was the perfect opportunity to implement the latest standards and best practices.

https://testmysite.thinkwithgoogle.com/

To start off, a few basics:

  • Serve a manifest.json that provides a set of icons, to allow compatible devices to add a shortcut to the website on their homescreen, same as a mobile app.
  • Service worker. This one is way more complex and deserves its own post (feel free to do some research in the meantime). In short, we used SW-toolbox to generate a first version of the service worker (we would totally use Workbox now), then added a few extra rules to allow intense assets caching, as well as HTML pages for offline access (but requesting the version of the network first for any change).
A snippet of the service-worker.js
  • Setting an aggressive server cache policy: All static assets (JS, CSS…) are named with a hash, making every new version of the file unique with a different name. We set a very long expire date for optimization. Brotli isn’t available on S3 yet, so we gzipped everything.
  • Using critical CSS: Not the best experience with Post CSS (would recommend Styled Components, we will probably use that in the future), but generating some (inline) critical CSS is essential for the first rendering.
    We also got inspired by this article wrote by Gajus Kuizinas to create a little class name generator that would update a dictionary of CSS class names during the builds, so they go from

to

It saved ~50% of the CSS bundle size, and a lot of KB on our HTML pages!

On top of the Javascript tips found in the awesome Cost of JS by Addy Osmani minifying everything, tree-shaking, scope hoisting…), we also focused on optimizing the application, especially for mobile. For instance, we:

  • Pre-render the DOM using redux-responsive with initialMediaType: mobile so the HTML generated shipped on the buckets is the mobile render.
  • Used the powerful Image API provided by Contentful, so we can request different types of format, size and quality depending of the context. We used webp when we could (or jpg with a 85 quality), and requested a different width depending of the current module and context (see LazyLoad++ section for more informations).

We constantly ran audits to make sure our fixes actually made the application better. We mostly used Lighthouse, which is maintained and improved every week (they also make it harder to pass after every updates, thank you guys!)

Lazyload++

Nothing new here, all the assets in the app are lazyloaded for performance improvements, but we also took advantage of the Contentful Image API + Chrome navigator.connection API to even further our process. We:

  • Used Intersection Observer to know which assets have to be loaded, with a fallback using getBoundingClientRect() + scrollY position. Huge performance gain noticed here by avoiding the consuming getBoundingClientRect() .
  • Stripped out all the <img /> tags / background-image from the SSR, so the first render doesn’t load all the assets at once.
  • Loaded different assets depending of the context. For instance on mobile, depending of your current connection and device ratio, we loaded a different size of the asset:

That provides a custom experience depending on the context a user is browsing the website.

A note about Accessibility

Chobani has a huge audience, so we wanted to make sure we made a website compliant with accessibility basics. There’s always room for improvements, but we found some clever ways to improve accessibility here and there worth noting.

For instance, we ran a contrast color check on the hex value coming from the CMS data between a background and a text colors, across all the modules of the website. If it didn’t pass the test, we provided a default color for the text based on the initial background color.

What’s next

Always more to do! Our priority is to:

  • Explore React-loadable to split the JS bundle and load only what’s necessary
  • Use Styled Components instead of Post CSS for future projects.
  • Use React v.16 to improve rendering time + remove data-reactid attribute in generated HTML (useless bytes)
  • Use Workbox instead of outdated SW-toolbox

This project allowed us to take our workflow to another level and set us on the path of releasing our first open source project. We also truly embraced accessibility and mobile best practices for a better user experience.

We’re looking forward to working on our next project and pushing even further our next technical challenges.👊