Timeline is Asana’s most complex visualization to date. Inspired by the classic Gantt chart and motivated by the same challenge of allocating work and resources, it is a feature-rich, interactive tool designed to help users plan, track, and allocate work over time within a project. To do so, users create, place, and move tasks arbitrarily on a 2-D scrolling canvas.
In the process of building Timeline, we encountered a variety of technical problems driven by the scope and ambition of the feature itself. As we ultimately moved from development towards launch, we also pivoted from one large launch day to an incremental launch, which was a successful way for us to mitigate risk, and a practice that has spread more widely throughout Asana.
When we first started iterating on Timeline, it was clear this would be a daunting technical challenge for our Engineering team. Two of our biggest technical challenges were:
- Reactivity: First and foremost, like all of Asana, Timeline would need to be completely reactive, updating the state of tasks, users, and projects dynamically based on changes from other people in a user’s team.
- Smooth rendering and scrolling: We would also need to maintain smooth performance when loading and displaying hundreds of tasks, dynamically size tasks based on zoom level as a user moved from weeks to months to years, and ensure smooth transitions as the data and views changed.
Given the scale and novelty of the work, having a smooth launch was a key goal from the start. We didn’t simply flip a switch and launch Timeline, zero-to-hero. Instead, we launched incrementally and coordinated with our infrastructure and scalability teams. This allowed us to catch big issues early and validate our assumptions about user behavior. Over time, Timeline arrived for each and every one of our customers, helping them plan and do better work, bringing new conversions for many, and increasing usage for more niche features that are better utilized and surfaced in Timeline. Most importantly, it worked for them without hiccups. At Asana, we focus on moving fast without breaking things, and we use incremental launches to larger and larger subsets of users to make our product steady while shipping powerful features out the door quickly.
Coping with technical hurdles in development
The requirements for Timeline charted new territory for Asana. Our user research showed how important zooming in and out would be to our users. They needed to use Timeline to see the right information, at the right scale. As we progressed with zooming and other UI elements, we realized animations needed to be a key part of our UI. In our early work, it was quite jarring to instantly change between different visualizations of the same data. Tasks jumped within the page without continuity between where they started and where they ended, while some – now outside the viewport – disappeared altogether! Animations helped us track the transition and visually explain to our users the new position and size of the tasks as the zoom level changes – it was the clear choice from a UX perspective. The technical challenge here was to make the animations for tens or even hundreds of tasks look good and fast.
To do so, we used an approach called FLIP (First Last Inverse Position) animations. The first step was to hook into our React front-end to detect when tasks’ positions changed, information we then applied in our animation. We created a higher order component (HOC) called “FLIP Container” that listens to changes in arbitrary props. When those props change – and before the React component updates – we save the position of the previous state and update the new state to the previous state. Then, we add a transition from the previous state to the next state with a timing function. Once the transitions progress, it appears as though the tasks are animating from their original position to the final position.
With this FLIP Container HOC, we can listen to many more types of changes than just zooming. We quickly got to reuse our FLIP container to implement other animations throughout Timeline, such as “un-doing” moving a task. Simply adding a FLIP Container that listened for position changes in the tasks brought us smooth animations there, too.
Rendering and scrolling
Our second major technical hurdle was handling and subscribing to potentially thousands of tasks. User research showed it was important for us to support projects with a range of due dates over a large timespan – on the order of years. Since our UI marks time along the x-axis, we render an element per unit of time. Rendering the first handful of tasks in a project could mean rendering thousands of DOM elements if they are far apart. Ultimately, this would require significant memory and computation on the client, reducing rendering speed dramatically.
In order to tackle this problem, we leveraged “infinite scrolling” (also called “virtual scrolling”). The key benefit of infinite scrolling is saving resources given large sets of data by only rendering what is visible by the user, plus some extra on the viewport “edges” for buffer. We track how far a user has scrolled in any direction. When the user scrolls further, we update our wrapper component’s state to filter out components that have moved outside the viewport.
We also built a “scrollable” wrapper component that listens for elements being added to the left of, and/or above, the viewport. When this happens, we calculate how much more space we’re prepending and update our scroll position so that the user experiences what appears to be one continuous smooth scroll, even though elements and our SVG canvas are dynamically updated on the fly. By loading an extra buffer slightly beyond what the user can see, we are able to minimize “flash” on the browser, as well. The filtering sharply reduces the number of interactive and non-interactive DOM elements, while dramatically improving the “feel” of Timeline.
As a final performance improvement, we added throttling to some of Timeline’s interactive behaviors. Because so many of our elements listen to scroll and mouse events, particularly when dragging and dropping tasks, we couldn’t process all of the computations required for each one as the browser fired events. Instead, we relied on the requestAnimationFrame() API to limit style changes to happen at most once per frame. Even with hundreds of scroll update computations in a single frame, only the last one would determine which styles to apply.
A few weeks before Timeline’s launch date, we noticed some performance issues that we hadn’t come across before. One of our original design goals for Timeline was to make it very flexible and free-form. We wanted to replace the process of placing sticky notes on a whiteboard with the ability to move items around on an infinite digital canvas. To create something so flexible, we needed to persist the vertical and horizontal locations of all the tasks in a Timeline in an unsorted, “free-form” view. When visiting Timeline for the first time for a project, every ‘position’ object needed to be initialized in the database. In the final stages of testing, it became clear that this initialization could be very inefficient, straining our infrastructure for very large projects.
We were really relieved to catch this issue in our early beta and A/B testing. Not only did we prevent some user pain, we were motivated to dig deeper with our Stability team to sort through all of the consequences of the new feature in terms of queries, space, cost, and traffic. If we had launched the way we had for many features in the past – 100% on launch day – we might not have hit this issue until it was too late. You might think the excitement from turning on a feature all at once was lost, but the confidence we had when launching with show-stopping issues ironed out in advance was plenty thrilling and rewarding to boot!