52

Scenario:

  1. In my site I display books.
  2. The user can add every book to a "Read Later" list.

Behavior:

When the user enters the site, they are presented with a list of books. Some of which are already in their "Read Later" list, some aren't.

The user has an indication next to each book telling them whether the book has been added to the list or not.

My issue

I am debating which option is the ideal for my situation.

Option 1:

  1. For every book, query the server whether it already exists in the user's list.
  2. Update the indicator for each book.

Pro: Very small request to the server, and very easy response (true or false).

Con: In a page with 30 books, I will send 30 separate http requests, which can block sockets, and is rather slow considering the browser and the server have to perform the entire handshake for each transaction.

Option 2:

  1. I query the server once, and get a response with the full list of books in the "Read Later" list as an array.
  2. In the browser, I go over the array, and update the indication for each book based on whether it exists in the array or not.

Pro: I only make one request, and update the indicator for all the books at once.

Con: The "Read Later" list might have hundreds of books, and passing a big array might prove slow and excessive. Especially in scenarios when not 30 books appear on the screen, but only 2-3. (That is, I want to check if a certain book is in the list, and for this I have the server send the client the entire list of books from the list).

So,

Which way would you go to maximize performance: 1 or 2?

Is there any alternative I am missing?

Michael Seltenreich
  • 3,013
  • 2
  • 29
  • 55
  • 8
    I think the question is borderline to "opinion answers", but I really appreciate the clear wording and problem description. – GhostCat Apr 29 '17 at 05:45
  • I appreciate your input, with that, I believe this is a useful question that is relevant to many topics. The question of 1 big request vs. many small ones. I truly believe it has a clear answer, or at least a few good answers that would help me, but also the community of SO – Michael Seltenreich Apr 29 '17 at 06:40
  • I'm voting to close this question as off-topic because it is asking about broad design issues and not about programing. – AdrianHHH Apr 29 '17 at 08:04
  • @AdrianHHH Where do you suggest I post it? Superuser? – Michael Seltenreich Apr 29 '17 at 08:15
  • The thing is: not every interesting / relevant question does fit the scope of this site. But that doesn't necessarily mean that there are other / better places to ask. – GhostCat Apr 29 '17 at 09:39
  • FYI: I added a non-link to my answer. Maybe that podcast episode is helpful for you, too. – GhostCat Apr 29 '17 at 10:19

4 Answers4

42

I think in 2017, and beyond, the solution is much less about overall performance but about user experience and user expectations.

Nowadays users do not tolerate delays. In that sense sophisticated user interfaces try to be responsive as quickly as possible. Thus: if you can use those small requests to enable the user to do something quickly (instead of waiting 2 seconds for that one big request to return) you should prefer that solution.

To my knowledge, there are many "high fidelity" sites out there where a single page might send 50, 100 requests. Therefore I consider that to be common practice!

And maybe it is helpful here: se-radio.net podcast episode 277 discusses this topic intensively, in the context of tail latency.

GhostCat
  • 137,827
  • 25
  • 176
  • 248
15

Option 1 sounds good but has a big problem in terms of scalability.

Option 2 mitigates this scalability problem and we can improve its design:

Client side, via javascript, collect only displayed book ids and query once, via ajax, for an array of read-later info, only for those 30 books. This way you still serve the page fast and request a small set of additional info, once with a single http request.

Server side you can further improve caching an in memory array of read-later ids for each user.

Michele P
  • 176
  • 4
11

Live Testing, Solution & Real-World Data

This answer is written in JavaScript, and includes easy to understand code examples.

Introduction

The OP asked what is the most efficient way to make requests to a "Read Later" API that each request requires to wait some time while the backend saves the book.

For this answer, I have created a demo of a "Read Later" API endpoint, every request waits randomly from 70-130 milliseconds for saving each book.

I am testing in all scenarios 30 books every time.

Finally, we will see the best results for each method by measuring professionally real runtime of every action we will take.

Synchronous Requests (OP's Option 1)

Here, we will run every call via JS, one after the other synchronously.

The code:

async function saveBooksSync() {
    console.time('save-books-sync');

    // creates 30 book IDs
    const booksIds = Array.from({length: 30}, (_, i) => i + 1);

    // creates 30 API links for each request
    const urls = booksIds.map(bookId => `http://localhost:7777/books/read-later?bookId=${bookId}`);

    for(let url of urls) {
        const response = await fetch(url);
        const json = await response.json();
        console.log(json);
    }
    console.timeEnd('save-books-sync');
}

enter image description here

Runtime: 3712.40087890625 ms

One Big Request

Although we will not be creating many request connection to the server, the runtime speaks for itself.

The code:

async function saveAllBooksAtOnce() {
    console.time('save-all-books')
    const booksIds = Array.from({length: 30}, (_, i) => i + 1);
    const url = `http://localhost:7777/books/read-later?all=1`;

    const response = await fetch(url);
    const json = await response.json();
    console.timeEnd('save-all-books');
}

enter image description here enter image description here

Runtime: 3486.71484375 ms

Parallel Asynchronous Requests (solution)

Here the magic happens, the solution to the question, what is the most efficient request method.

Here we are making 30 parallel small requests with amazing results.

The code:

async function saveBooksParallel() {
    console.time('save-books')
    const booksIds = Array.from({length: 30}, (_, i) => i + 1);
    const urls = booksIds.map(bookId => `http://localhost:7777/books/read-later?bookId=${bookId}`);

    const promises = urls.map((url) =>
        fetch(url).then((response) => response.json())
    );

    const data = await Promise.all(promises);

    console.log(data);
    console.timeEnd('save-books');
}

enter image description here

Here in this asynchronous parallel example, I used the Promise.all method.

The Promise.all() method takes an iterable of promises as an input, and returns a single Promise that resolves to an array of the results of the input promises

Runtime: 668.47705078125 ms

Conclusion

The results are clear, the most efficient way to make these multiple requests is to do this in Asynchronous Parallel.

Update: I followed @Iglesias Leonardo's request to remove the console.log() of the data output because (presumably) it takes high resources.

These are the runtime results:

  • Synchronous Requests: 3371.695 ms
  • One Big Request: 3358.269 ms
  • Parallel Asynchronous Requests: 613.506

Update Conclusion: The runtimes stayed almost the same and thus reflect the reality that Parallel Asynchronous Requests are unmatched by speed

enter image description here

Stas Sorokin
  • 3,029
  • 26
  • 18
2

In my view it depends on how the data is stored. If a relational database is being used you could easily get the boolean flag into the list of books by simply doing a join on the corresponding tables. This will most likely give you the best results and you wouldn't have to write any algorithms in the front end.

  • I think you missunderstood my question. I have no issue with querying for data, but about transporting it from the server to the end user. – Michael Seltenreich Apr 29 '17 at 05:19
  • I think his point was that if a query for 30 books is fast in the backend there is nothing that speaks against a http call that contains all of them in a single request – Marged Apr 29 '17 at 05:59
  • 30 is not the problem, 300 is. or maybe it isn't? – Michael Seltenreich Apr 29 '17 at 06:39
  • "Is this id in the intersection of these two sets (being displayed and read books)" is really a non-issue with relational databases. If you add pagination and only display about 30 or so books at a time, you should hardly see a difference. Introducing some clever chaching mechanism as suggested by @MicheleP should have you all set in terms of scalability. – DaSourcerer May 01 '17 at 08:59