🛠️ Dota Heroes: Heroes page refactor

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Dota Heroes: Heroes page refactor

There are a couple of things we can do better in this code.

Concurrent requests

When searched for the hero’s abilities, we sent two fetch requests – one after another.

zlFetch(`${dotaApi}/constants/abilities`)
  .then(response => {
    // ...
    zlFetch(`${dotaApi}/constants/hero_abilities`)
      .then(response => {
        // ..
      )}
  )}

The truth is, we don’t need to send these two requests separately. The second request doesn’t depend on the first one.

We can send them with Promise.all.

Promise.all([
  zlFetch(`${dotaApi}/constants/abilities`),
  zlFetch(`${dotaApi}/constants/hero_abilities`)
]).then(responses => {
  // ...
})

responses here is an array of responses from the requests. The order of responses follows the order we wrote in Promise.all.

This means:

  • First item is the response from the abilities endpoint.
  • Second item is the response from the hero_abilities endpoint.
Promise.all([
  zlFetch(`${dotaApi}/constants/abilities`),
  zlFetch(`${dotaApi}/constants/hero_abilities`)
]).then(responses => {
  const allAbilities = responses[0].body
  const heroesAbilities = responses[1].body
})

We can then proceed with filtering, mapping, and inserting the hero’s abilities into the DOM.

Promise.all(/* ... */)
  .then(responses => {
    // ...

    const heroAbilities = heroesAbilities[`npc_dota_hero_${heroName}`]
      // ...
      .join('')

    heroAbilitiesEl.innerHTML = heroAbilities
    heroAbilitiesEl.closest('section').removeAttribute('hidden')

Why not wrap all fetch requests in one Promise.all?

Good question! It certain makes sense to wrap all fetch requests into a single Promise.all function from a code point of view. This makes the code simpler and easier to maintain. But should we really do this?

When we are requesting for multiple resources at the same time, we need to think about how the content gets loaded. To do this, we look at the Network tab inside the Dev tools. You should see a list of the resources we requested.

List of requested resources in the Network tab.

I want you to pay special attention to the Waterfall column. This column tells us three things for each request:

  1. When the request is sent out (beginning of the green bar)
  2. How long it took to receive a response (the green bar)
  3. How long it took to download the data from the response (the blue bar)
Waterfall explanation.

Notice this: The hero image’s request is sent out immediately after we receive information from the heroes endpoint.

Image download starts immediately after heroes endpoint.

Wrapping requests together normally isn’t a huge problem because most requests are small. However, in this case, the abilities request is large. It is 1.13MB while the rest are approximately 100kb.

Size of the abilities response.

If we wrap the requests together in Promise.all, we can only begin downloading the hero’s image after we finished downloading the data from the abilities response. This takes considerably longer, which means users have to wait longer for the page to be populated.

Image download begins only after all four resources are fetched.

If we want to improve loading performance, we have to perform separate fetch requests.

Minimizing Reflows

Reflows refer to layout changes that causes content to jump. One of the tenets in web development is to minimise reflows so users have a better experience.

Our code right now produces up to three layers of reflows. This large amount of reflows is caused by the difference in loading time between different resources.

We need to consider the content-loading order to minisize reflows, so I want to turn your attention to the Network tab once again.

Here, you’ll want to refresh the page and few times and see the order of requests and how the waterfall gets fulfilled.

I see the following patterns:

  1. The hero’s image takes more time to load compared to hero_lore. This slow loading time causes a reflow in the hero’s description.
  2. Occasionally, abilities will load before lore, which causes the abilities section to reflow.

Preventing the hero’s description from reflowing

There are two ways to prevent the hero’s description from reflowing:

  1. We fix the image’s width and height so the description doesn’t have to reflow.
  2. We wait for the image to load before showing the hero’s description

The first method is better since we reduce load time. It’s also easier to execute.

The hero’s image’s width and height is 256px and 144px respectively. We can set these values directly in the CSS.

.hero-page .hero__img {
  /* ... */
  width: 256px;
  height: 144px;
}

Once you set these values, you’ll notice a border when the hero image isn’t loaded yet. This looks awful.

This border appears because the src attribute is empty. Browsers are treating the image as “not found” and showing a border to express this.

To prevent the border from showing up, we can insert a transparent image as the initial src.

<h1 class="site-title" data-hero-name></h1>
<img
  class="hero__img"
  src="images/transparent.png"
  data-hero-image
/>
<p data-hero-description></p>

Note: A better choice is to provide a placeholder image for the Hero’s name, image, and description. This is called a skeleton screen. Here’s Facebook’s skeleton for a post.

Facebook's skeleton

You can find more about skeleton screens here.

Prevent abilities from loading before description

Is this necessary? That’s the first question I always ask myself when creating features.

In this case, I suggest you don’t worry about preventing abilities from loading before the hero’s description because of two reasons:

  1. It doesn’t happen often.
  2. Even if it happens, the reflow doesn’t really affect the user negatively.

If you really wish to include this feature, you can send the request for abilities after you receive a response from the hero_lore endpoint.