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.
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),
};
}
});
}
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.