November 10, 2022

Optimistic view updates for latency compensation

In this tutorial, you'll learn how to keep your web app feeling fast even when your backend or connection is slow, using a technique called optimistic updating or latency compensation. The basic idea is that you update the view before calling the server, optimistically assuming things will work out. If there is an error, you need to revert the update and notify the user.

You can find the source code for the example project on GitHub.

Video version

The problem: A slow backend or network

The application you are building has a commenting feature. The problem is that the backend is slow. In real life, the slowness may be caused by a bad network connection or by the backend calling different services that are slow to respond. In this example, we simulate a slow backend with a three-second delay.

public Comment addComment(int articleId, Comment comment) throws InterruptedException {
  // Pretend the save takes long, then return a simulated saved comment
  Thread.sleep(3000);
  return new Comment(faker.number().randomDigit(), comment.username,
      comment.comment + " (saved)");
}

When adding a comment without optimistic updates, you would call the backend, wait for the response, and then update the view state.

async submitComment() {
  this.binder.submitTo(async (comment) => {
    if (!this.article) return;
    try {
      const saved = await ArticleEndpoint.addComment(this.article.id, comment);
      // Add the saved comment to the article
      this.article = {
        ...this.article,
        comments: [...this.article.comments, saved],
      };
      this.binder.clear();
    } catch (e) {
      console.log(e);
    }
  });
}

To the user, this looks like the app freezes for 3 seconds, only displaying a progress indicator at the top - not ideal.

Without optimistic updates, it takes seconds for the comment to show.

The solution: Update the view before calling the server

When you are performing an action with a high probability of success, like adding a new comment, you can make the app feel faster by optimistically updating the view state already before calling the server. The user gets instant feedback on their action.

async submitComment() {
  this.binder.submitTo(async (comment) => {
    if (!this.article) return;
    try {
      // Show the unsaved comment
      this.article = {
        ...this.article,
        comments: [...this.article.comments, comment],
      };
      // Call the backend
      const saved = await ArticleEndpoint.addComment(this.article.id, comment);
      // Swap out the unsaved comment for the saved comment
      this.article = {
        ...this.article,
        comments: this.article.comments.map((c) => (c === comment ? saved : c)),
      };
      this.binder.clear();
    } catch (e) {
      console.log(e);
      // Remove the unsaved comment on failure
      this.article = {
        ...this.article,
        comments: this.article.comments.filter((c) => c !== comment),
      };
    }
  });
}

With optimistic updates, the comment is displayed immediately.

When should you use optimistic updates?

The cost of optimistic updates is added complexity, so use them sparingly and only in situations where they make a clear UX impact. You may want to start with simple updates throughout your codebase and then optimize the places where you see the biggest potential benefits. Optimistic updates are typically safest in situations where you are adding new content or editing something private to the user - situations where merge conflicts are unlikely.

Resources

You can find the full source code for this example on GitHub.

© 2022 Vaadin. All rights reserved