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 qequests
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
- Time-based expiration: Delete cached data after a set duration.
- Manual triggers: Invalidate after mutations (e.g., when a user updates their profile).
- Vue’s reactivity: Reset data when the app’s state changes (like on logout).
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>
</div>
</div>
</template>
Advanced tips for loader design
- Match content structure: Design skeletons to mirror your actual layout (e.g., image placeholders, text lines).
- Prioritize above-the-fold content: Show loaders for visible content first while background data finishes.
- Combine with caching: If cached data exists, skip skeletons and show data immediately.
Why this matters
- User Experience: 74% of users notice load times (PWA Stats).
- Perceived Performance: Loaders make waits feel 15-30% shorter (NNGroup Research).
- Brand Trust: Polished transitions signal professionalism.
Putting it all together
By combining parallel requests, smart caching, and skeleton loaders, we were able to achieve significant improvements:
- Faster initial loads: Parallel requests cut data load times significantly.
- Smoother navigation: Reused responses reduced redundant network calls.
- 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.