Improving web interface responsiveness often starts with optimizing how your frontend communicates with the backend. In our Vue.js-based Semaphore UI, we introduced several techniques to make data loading faster and smoother for users. This post shares the three most impactful changes we implemented in version 2.14.


Why optimize API usage?

APIs are often the backbone of dynamic web apps, but poorly managed requests can create bottlenecks. Long wait times for sequential calls or redundant data fetching degrade user experiences. Let’s tackle these issues head-on.


1. Parallel requests with Promise.all()

The problem: Sequential requests

A common pitfall is making API calls one after another. For example, fetching user details, then their orders, then their preferences:

// Sequential approach (slow 😞)
async function fetchData() {
  const user = await getUser();
  const inventory = await getInventory(projectID);
  const templates = await getTemplates(projectID);
  return { user, inventory, templates };
}

Here, each request waits for the previous one to finish. If each call takes 200ms, the total is 600ms—a noticeable delay.

The solution: Parallel execution

Vue.js leverages JavaScript’s asynchronous capabilities, so we can fire multiple requests simultaneously using Promise.all():

// Parallel approach (fast ⚡)
async function fetchDataParallel() {
  const [user, inventory, templates] = await Promise.all([
    getUser(),
    getInventory(projectID),
    getTemplates(projectID),
  ]);
  return { user, inventory, templates };
}

By grouping independent requests, all three calls resolve concurrently. If each takes 200ms, the total drops to 200ms!

Integrating with Vue

In a Vue component, use this pattern in lifecycle hooks or methods:

export default {
  props: {
    projectID: Number,
  },
  watch: {
    async projectID() {
      await this.fetchDataParallel();
    },
  },
  async created() {
    await this.fetchDataParallel();
  },
  methods: {
    async fetchDataParallel() {
      try {
        const [
          this.user, 
          this.inventory,
          this.templates
        ] = await Promise.all([
          getUser(),
          getInventory(this.projectID),
          getTemplates(this.projectID),
        ]);
      } catch (error) {
        console.error("Failed loading data:", error);
      }
    };
  }

Tip: Combine this with skeleton loaders to keep users engaged while data loads.


2. Reusing cached API responses

The problem: Redundant fetching

Apps often re-fetch the same data across components (e.g., user profile data requested by multiple views). This wastes bandwidth and slows down the interface.

The solution: Cache and reuse

Store API responses and reuse them until invalidated. Vue’s reactivity system makes this easy with a centralized store (like Pinia or Vuex):

Step 1: Create a cache store

// stores/apiCache.js
import { defineStore } from 'pinia';

export const useApiCache = defineStore('apiCache', {
  state: () => ({
    project: null,
    templates: {},
  }),
  actions: {
    async fetchProject() {
      if (!this.project) {
        this.project = await getProject();
      }
      return this.project;
    },
    async fetchTemplates(id) {
      if (!this.templates[id]) {
        this.templates[id] = await fetchTemplates(id);
      }
      return this.templates[id];
    },
  },
});

Step 2: Use the Store in Components

// Component.vue
import { useApiCache } from '@/stores/apiCache';

export default {
  setup() {
    const apiCache = useApiCache();

    // Reuses cached user or fetches once
    const user = apiCache.fetchUser();

    return { user };
  },
};

Tip: In Semaphore UI, we don’t use Pinia yet, but we plan to migrate to it soon.

Cache invalidation strategies

3. Using skeleton loaders for smoother transitions

The problem: Unresponsive UI during loading

Even with optimized API calls, users still face brief delays while data loads. Blank screens or frozen interfaces during this time can make your app feel unpolished or slow, even if the actual fetch time is minimal.

The solution: Skeleton loaders

Skeleton placeholders mimic your content’s layout while data loads, providing visual feedback that keeps users engaged. Combined with parallel requests and caching, they create a seamless perception of speed.


How to add skeleton loaders in Vue.js

Step 1: Choose skeleton component

In Semaphore UI we use Vuetify framework. It already includes skeleton loader component.

<v-skeleton-loader
  type="
    table-heading,
    image,
    list-item-two-line,
    list-item-two-line,
    list-item-two-line"
></v-skeleton-loader>

Step 2: Integrate with async data fetching Use a loading state to toggle between skeletons and real content:

// Component.vue
export default {
  data() {
    return {
      isLoading: true,
      user: null,
      templates: [],
    };
  },
  async created() {
    this.isLoading = true;
    try {
      [this.user, this.templates] = await Promise.all([
        fetchUser(),
        fetchTemplates(),
      ]);
    } finally {
      this.isLoading = false;
    }
  },
};

Step 3: Conditionally render skeletons

<template>
  <div class="user-profile">
    <v-skeleton-loader
      v-if="isLoading"
      type="
        table-heading,
        image,
        list-item-two-line,
        list-item-two-line,
        list-item-two-line"
    ></v-skeleton-loader>
    <v-card v-else>
      <h2>{{ user.name }}</h2>

      <v-card v-for="tpl in templates" :key="tpl.id">
        <v-card-title>{{ tpl.name }}</v-card-title>
        <v-card-text>{{ tpl.description }}</v-card-text>
      </v-card>
    </v-card>
  </div>
</template>

Advanced tips for loader design

  1. Match content structure: Design skeletons to mirror your actual layout (e.g., image placeholders, text lines).
  2. Prioritize above-the-fold content: Show loaders for visible content first while background data finishes.
  3. Combine with caching: If cached data exists, skip skeletons and show data immediately.

Why this matters


Putting it all together

By combining parallel requests, smart caching, and skeleton loaders, we were able to achieve significant improvements:

  1. Faster initial loads: Parallel requests cut data load times significantly.
  2. Smoother navigation: Reused responses reduced redundant network calls.
  3. Enhanced user engagement: Skeleton loaders improved visual feedback and perceived speed.

Optimizing API usage isn’t just about raw performance—it also improves user experience. We’ll continue refining our frontend to make Semaphore UI faster and friendlier with every update.