Tinder recently swiped right on the web. Their new responsive Progressive Web App – Tinder Online – is available to 100% of users on desktop and mobile, employing techniques for JavaScript performance optimization, Service Workers for network resilience and Push Notifications for chat engagement. Today we’ll walk through some of their performance learnings.
Journey to a Progressive Web App
Tinder Online started with the goal of getting adoption in new markets, striving to hit feature parity with V1 of Tinder’s experience on other platforms. The MVP for the PWA took 3 months to implement using React as their UI library and Redux for state management.
The result of their efforts is a PWA that delivers the core Tinder experience in 10% of the data-investment costs for someone in a data-costly or data-scarce market (2.8MB):
Early signs show good swiping, messaging and session length compared to the native app. With the PWA:
- Users swipe more on web than their native apps
- Users message more on web than their native apps
- Users purchase on par with native apps
- Users edit profiles more on web than on their native apps
- Session times are longer on web than their native apps
 Tinder are looking forward to sharing more data about the business metrics from their PWA in the future.
Performance
The mobile devices Tinder Online’s users most commonly access their web experience with include:
- Apple iPhone
- Apple iPad
- Samsung Galaxy S8
- Samsung Galaxy S7
- Motorola Moto G4
Using the Chrome User Experience report (CrUX), we’re able to learn that the majority of users accessing the site are on a 4GÂ connection:
Note: Rick Viscomi recently covered CrUX on PerfPlanet and Inian Parameshwaran covered rUXt for better visualizing this data for the top 1M sites.
Testing the new experience out on WebPageTest using the Galaxy S7 on 4G we can see that they’re able to load and get interactive in 5.9 seconds:
There is of course to improve this further on median mobile hardware (like the Moto G4) as we can see from WebPageTest, however Tinder are hard at work on optimizing their experience and we look forward to hearing about their work on web performance in the near future.
Performance Optimization
Tinder were able to improve how quickly their pages could load and become interactive through a number of techniques. They implemented route-based code-splitting, introduced performance budgets and long-term asset caching.
Route-level code-splitting
Tinder initially had large, monolithic JavaScript bundles that delayed how quickly their experience could get interactive. These bundles contained code that wasn’t immediately needed to boot-up the core user experience, so it could be broken up using code-splitting. It’s generally useful to only ship code users need upfront and lazy-load the rest as needed.
To accomplish this, Tinder used React Router and React Loadable. As their application centralized all their route and rendering info a configuration base, they found it straight-forward to implement code splitting at the top level. Â
In summary:
Use React Loadable to split out non-critical JavaScript into separate bundles.Â
Use the webpack CommonsChunkPlugin to move common items up to a single bundle file.
They use React Loadable’s preload support to preload potential resources for the next page on control component.
Tinder Online also uses Service Workers to precache all their route level bundles and include routes that users are most likely to visit in the main bundle without code-splitting.
Impact
After introducing route-based code-splitting their main bundle sizes went down from 166KB to 101KB and DCL improved from 5.46s to 4.69s:
Long-term asset caching
Ensuring long-term caching of static resources output by webpack benefits from using [chunkhash] to add a cache-buster to each file. Tinder were using a number of open-source (vendor) libraries as part of their dependency tree. Changes to these libraries would originally cause the [chunkhash] to change and invalidate their cache.Â
To address this, Tinder began defining a whitelist of external dependencies and splitting out their webpack manifest from the main chunk to improve caching. The bundle size is now about 160KB for both chunks.
Preloading
As a refresher, link rel=preload is a declarative instruction to the browser to load critical, late-discovered resources earlier on. In single-page applications, these resources can sometimes be JavaScript bundles.
Tinder implemented support for link rel=preload to preload their critical JavaScript/webpack bundles that were important for the core experience. This reduced load time by 1s and first paint from 1000ms to about 500ms.Â
Performance budgets
Tinder adopted performance budgets for helping them hit their performance goals on mobile. As Alex Russell noted in “Can you afford it?: real-world performance budgets“, you have a limited headroom to deliver an experience when considering slow 3G connections being used on median mobile hardware.Â
To get and stay interactive quickly, Tinder enforced a budget of ~155KB for their main and vendor chunks, asynchronous (lazily loaded) chunks are ~55KB and other chunks are ~35KB. CSS has a limit of 20KB. This was crucial to ensuring they were able to avoid regressing on performance.
Webpack Bundle Analysis
Webpack Bundle Analyzer allows you to discover what the dependency graph for your JavaScript bundles looks like so you can discover whether there’s low-hanging fruit to optimize.Â
Tinder used Webpack Bundle Analyzer to discover areas for improvement:
- Polyfills: Tinder are targeting modern browsers with their experience but also support IE11 and Android 4.4 and above. For polyfills, they are including core-js and use babel-preset-env to remove unused polyfills.
- Slimmer use of libraries: Tinder replaced localForage with direct use of IndexedDB
- Better splitting: Split out components from the main bundles which were not required for first paint/interactive
- Created asynchronous common chunks to abstract chunk use more than three times from children
- Tinder also removed critical CSS from their core bundles (as they had shifted to server-side rendering and delivered this CSS anyway)
This also included taking advantage of Webpack’s Lodash Module Replacement Plugin. The plugin creates smaller Lodash builds by replacing feature sets of modules with noop, identity or simpler alternatives:
Webpack Bundle Analyzer can be integrated into your Webpack config and Tinder’s setup looks like this:
plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'server', analyzerPort: 8888, reportFilename: 'report.html', openAnalyzer: true, generateStatsFile: false, statsFilename: 'stats.json', statsOptions: null }) ]
The majority of the JavaScript left is the main chunk which is trickier to split out without architecture changes to Redux Reducer and Saga Register.
CSS Strategy
Tinder are using Atomic CSS to create highly reusable CSS styles. All of these atomic CSS styles are inlined in the initial paint and some of the rest of the CSS is loaded in the stylesheet (including animation or base/reset styles). Critical styles have a maximum size of 20KB gzipped, with recent builds coming in at a lean < 11KB.
Tinder use CSS stats and Google Analytics for each release to keep track of what has changed. Before Atomic CSS was being used, average page load times were ~6.75s. After they were ~5.75s.
Runtime performance
Deferring non-critical work with requestIdleCallback()
To improve runtime performance, Tinder opted to use requestIdleCallback() to defer non-critical actions into idle time. This included work like instrumentation beacons. They also simplified some HTML composite layers to reduce paint count while swiping.
Before using requestIdleCallback() for their instrumentation beacons:
and after..
Dependency upgrades
In older versions of webpack, when bundling each module in your bundle would be wrapped in individual function closures. These wrapper functions made it slower for your JavaScript to execute in the browser. Webpack 3 introduced the ability to concatenate the scope of all your modules into one closure and allow for your code to have a faster execution time in the browser. It accomplished this using:
new webpack.optimize.ModuleConcatenationPlugin()
This is called scope hoisting. This improved initial JavaScript parsing time for Tinder’s vendor bundle by 8%. Updating to the latest version of React (React 16) also reduced the size of their vendor chunk by ~6.7%.
Workbox for network resilience and offline asset caching
Tinder also use the Workbox Webpack plugin for caching both their Application Shell and their core static assets like their main, vendor, manifest bundles and CSS. This enables network resilience for repeat visits and ensures that the application starts-up more quickly when a user returns for subsequent visits.
Opportunities
Digging into the Tinder bundles using source-map-explorer (another bundle analysis tool), there are additional opportunities for reducing payload size. Before logging in, components like Facebook Photos, notifications, messaging and captchas are fetched. Moving these away from the critical path could save up to 20% off the main bundle:
Another dependency in the critical path is a 200KB Facebook SDK script. Dropping this script (which could be lazily loaded when needed) could shave 1 second off initial loading time.
Conclusions
Tinder are still iterating on their Progressive Web App but have already started to see positive results from the fruits of their labor. I’m excited to hear more about their progress in the near future!
With thanks and congrats to Roderick Hsiao, Jordan Banafsheha, and Erik Hellenbrand for launching Tinder Online and their input to this article. Thanks to Cheney Tsai for his review too.