13

Preparing Transmission’s Web Frontend for Scale

 3 years ago
source link: https://flexport.engineering/preparing-transmissions-web-frontend-for-scale-855505b68c78
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Preparing Transmission’s Web Frontend for Scale

How we used the Chrome Profiler to prepare our fleet GPS map for larger trucking carriers.

1*94p7kIbdD9DHEsKQP2HBzA.png

Transmission, a Flexport venture, has built a software suite for trucking carriers. Employees at these carriers use different features of Transmission according to their job function:

  • Fleet managers at the office use Transmission’s web app to gain real-time visibility into the movement of their trucks, including live delivery updates and GPS tracking.
  • Drivers on the road share detailed delivery updates from the mobile app, which is also a certified electronic logging device for hours of service compliance tracking.

Transmission is built by our Chicago-based trucking team. For most of the last year, the team has been focused on building out the initial version of the product and responding to customer feedback. Transmission’s early adopters so far have been relatively small carriers with around 20 trucks in their fleet. Last month, we began preparations to onboard some larger carriers with fleets of 200+, which involved load testing Transmission to identify any performance issues that came with scale.

And it’s a good thing we did!

We discovered that our Fleet Map component (a React wrapper around mapbox-gl) became sluggish when displaying more than 50 trucks. Simple interactions like dragging the map or hovering over markers would turn to molasses ? There were clear client-side performance problems to be addressed.

This post will cover the steps that were taken to diagnose and fix the observed performance issues with our map. This not only resulted in a buttery smooth map experience, but also an open source contribution to address a bug in mapbox-gl!

Diagnosing Problems

Being new when this effort began, I didn’t really know where to start looking for performance bottlenecks (other than the top-level page component itself).

Without additional context about the code, I needed data to direct my focus.

After simulating data for around 200 trucks to put the map under load, I used the Chrome Profiler to identify sub-components and even lines of code that were causing problems without needing to know anything else about the structure of the code itself.

This post won’t deep dive into profiling basics — you can find plenty of that elsewhere — but it will still cover the pieces of the collected profiles that piqued my interest and sparked additional investigation.

First Map Interaction: Slow Hovering

The issue I focused on first was the noticeable slowness when hovering on Truck Cards in the scrollable card list to the left of the map:

1*GmIz7l9XdHvZ5bPpS3ycEA.gif

I collected the following profile by recording a single card hover event like in the gif above.

0*GeTCyf4dACfXANEs

Two different patterns popped out at me.

Pattern A: Three Full Re-Renders

0*u5hepBdgD-5BZSHw

Each of the three “Pattern A” segments represented a re-render of the entire page.

A single re-render would have made sense to me: when a card is hovered, the corresponding marker on the map should show its tooltip. This behavior was implemented via a piece of shared state in a parent component of both truck cards and map markers. The currently hovered truck is updated in state hover, and is ultimately passed as a prop to both cards and markers to keep them in sync.

But why would a single hover interaction cause the page to re-render three times?

The answer wound up being due to some subtle differences between how onMouseEnter and onMouseOver events propagate to event handlers.

Originally, in the Truck Card component, we were using an event handler subscribed to onMouseOver events to respond to the hover interaction on a card. I learned that onMouseOver events are generated whenever the mouse moves over any sub-component in a composition, and that these events bubble up to event handlers defined on parents.

Here is a diagram of the different sub-components defined in a Truck Card:

0*EvYG4nRp0kEpAASv

Each green box is a sub-component

Whenever any sub-component was moused over, the onMouseOver event handler defined on the outermost wrapper in the card composition would be triggered, which would cause the entire page to render again and again…

Solution: Respond to Hovering by Subscribing to onMouseEnter

I learned that onMouseEnter events do not have the same bubbling behavior as onMouseOver.

From the React docs:

The onMouseEnter and onMouseLeave events propagate from the element being left to the one being entered instead of ordinary bubbling and do not have a capture phase.

Simply changing the event to which we were subscribing to onMouseEnter caused our event handler to trigger once per card hover instead of once per sub-element, which is exactly the behavior we wanted ?

Implementing this change cut the time for hover interactions by 2/3rds, and resulted in a flame graph that looked like this:

0*dfc8McWImnfr1WRt

Check out the JSFiddle below to see the difference between onMouseEnter and onMouseOver for yourself.

Pattern B: Addressing Rendering That Scaled with N

Zooming in on the remaining chunk of profiler output, “Pattern B” indicated that render time for hovering was scaling up with the number of trucks rendered to the page.

1*uqtT0HViyNI4OMKLJg5oqQ.png

I had simulated 200 trucks for load testing purposes, and Pattern B looked to be around 200 peaks wide. I decided to check out what the leftmost “Pattern B” looked like with four trucks:

1*ZZ-kIwVPbXDme6GK5ehgTQ.png

Four peaks instead of 200. Scaling problem confirmed ✅

The leftmost “Pattern B” chunk was being caused by the re-rendering of Truck Card components in the card list, while the rightmost chunk was being caused by the re-rendering of tooltips in the map component. Each card was taking 2.5ms to re-render, resulting in 500ms of time wasted. This was longer than the amount of time spent re-rendering tooltips, so I decided to focus on cutting out unnecessary card renders first.

So what could have been causing every truck card to re-render, even though only one was being hovered? The answer: Unintentional prop changes.

Truck cards were composed of several layers of components, some of which were getting inline functions passed to them as props.

Inline functions passed as props can cause problems: they will be re-defined each time a component renders, making it seem as if props are changing even when they are not. This can lead to a lot of unintentional re-renders.

Solution: use pureComponent principles to reduce renders

Two possible paths towards fixing an issue like this:

  1. Add reflective-bind to your JavaScript build process, as described in the blog post Ending the Debate on Inline Functions in React.
  2. Extract inline functions into class methods so they are defined one time and never change.

We favor reflective-bind, since it allows us to keep the readability of inline functions while still getting the render-saving benefits of defining functions once.

I also wrapped the Truck Card component in React.memo, which provides shallow prop comparison during the shouldComponentUpdate phase of the component lifecycle. When React components are designed to behave like pure functions (same props in === same rendered output) using React.memo — or React.pureComponent for class components — can save a lot of needless render time.

Note: Be careful when defining anonymous event handlers in a stateless functional component! You won’t see the benefits of reflective-bind unless your component extends React.pureComponent, is wrapped in React.memo, or otherwise implements shallow prop comparison in the shouldComponentUpdate phase of the component lifecycle.

Using pureComponent principles helped reduce unintentional re-rendering in Truck Card components, resulting in a flamegraph that ultimately looked like this:

1*uDrisYE6Stxd6WoikNrigQ.png

The entire first “Pattern B” section went from 200 peaks in the flamegraph down to 1 peak (just the card being hovered).

After implementing all of the changes discussed above, hovering over truck cards on the fleet map page went from 3 seconds to 250ms.

That’s an improvement of over 90% ? ? ?

1*p_O6t5QYJX0vbDp0LkahkA.gif

Second Map Interaction: Sluggish Movement

A second sluggish interaction was poor frame rate when dragging and zooming the map.

1*0rcSpJcyt8xwCyEXaHgWwg.gif

A quick run of the profiler revealed the following information in the “Bottom-Up” tab of the activities panel after sorting by Total Time (which indicates where scripting time is being spent):

1*469JiHV5ujclDBRVsGt9Ww.png

It was clear from the activities above that a lot of time during drag and zoom interactions was being spent in mapbox-gl’s render method. This appeared to originate from a component called TruckMarker.jsx (selected in blue in the image).

Taking a look at the TruckMarker.jsx code revealed the following implementation:

TruckMarkers were comprised of a map pin, and a corresponding tooltip that showed on hover of either the marker or the corresponding TruckCard. Each marker was maintaining its own piece of state isShowing, to control whether or not their tooltip was visible… when tooltips are showing on the map, and a zoom or drag event is initiated, tooltips need to be hidden so they don’t appear to float away from the original marker position.

In order to respond to map movement, every single marker component was subscribed to the mapbox-gl’s render event with an event handler that called this.setState({isShowing: false}) to hide any tooltips that were shown… Yet again, it makes sense that we didn’t notice this performance problem until the map started scaling up with more trucks:

more data => more markers on the map => more subscribers => more event handlers to execute

Not only were there too many subscribers, but each marker was subscribed to a particularly noisy event: mapbox-gl’s render event is emitted whenever the map moves AND whenever a new tile is loaded. Tiles loading with additional detail are irrelevant to whether or not markers should hide their tooltips. Only movement matters.

Two changes were identified to address these problems:

Change 1: Reduce Event Subscribers by Refactoring Where isShowing State is Stored.

This was the a huge time saver.

If you remember from the hover discussion, we were already storing whether a particular truck was being hovered in the common parent of both markers and cards. Using this available piece of state, it was easy enough to compute a value for isShowing to pass in to each marker as a prop: just check whether the ID of the marker matches the id of the truck being hovered.

By moving the responsibility of managing tooltip visibility into the parent, only the parent component needed to be subscribed to mapbox-gl events thus reducing the number of mapbox event subscribers from N to 1.

Change 2: Subscribe to mapbox-gl move Events Instead of render

mapbox-gl provides a bunch of different events for subscription. After learning how noisy render was, I discovered the move event, which is only emitted during animated transitions between views (e.g. drag events, and zoom events).

By changing our subscription from render to move, we were able to eliminate even more unnecessary calls to our subscribed event handler, since move is not emitted when new tiles load.

This reinforced one of the biggest learnings from this whole performance experience for me: take time to understand the behavior of emitted events before subscribing to them. Simple changes like swapping from render to move, or from onMouseOver to onMouseEnter had dramatic effects on the performance of our map.

After both of the above changes were implemented, map movement was much smoother ?

1*0IaBdSn8HE7p0XgUr_cqsg.gif

An Open Source Contribution

While researching mapbox-gl event alternatives, I stumbled upon some unexpected behavior in the emit frequency of movestart and moveend events. They are supposed to emit once at the start and end of map movement respectively, but I found that movestart was being emitted tens of times while zooming.

My next step was to create a reproduction of the issue outside of our codebase: check out the JSFiddle below, and notice how the movestart counter above the map increments quickly on zoom, but not on drag.

Repro in hand, I opened an issue with mapbox-gl to get confirmation that this was actually unintended behavior. The maintainers agreed, it was indeed a bug ?

Finally, I opened a PR with a fix + tests, which was merged a few days later. When the new version of mapbox-gl releases with this fix, I should be able to simplify the logic in our mapbox event subscriber even more ?

Kudos to the mapbox-gl team for maintaining an awesome open source project, and community!

Knowing When to Stop Performance Tuning

An important step of any performance tuning effort is knowing when to stop: what is considered “good enough” so the team can move on to higher priority work?

For example: you’ll notice that there is still an instance of “Pattern B” in the final flamegraph for the hover animation (right side of the flamegraph). This is being caused by the unnecessary re-rendering of tooltips on the map itself. Could we have spent more time trying to reduce re-renders here? Sure. But the gains wouldn’t have been as dramatic, and the map already met performance requirements for our larger clients ?

In the end, it’s up to the team to make the call of when to stop performance tuning. We collected additional improvement opportunities (like adding mapbox clustering) in our backlog so they aren’t forgotten, and moved on to higher priority work.

Help Us Build the Operating System for Global Trade

If you’re passionate about performance, React, Rails, GraphQL, or are interested in helping make global trade more accessible for everyone, we’re hiring! We have open engineering positions in Chicago and San Francisco, and openings for all other functional areas in any of our offices around the world ?

</div


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK