· 5 min read
Speeding Up Telegram Mini Apps with Request Deferment and Progressive Hydration
Telegram Mini Apps often feel slow not because of one big bottleneck, but because too much work happens before the first usable screen appears. This tutorial shows how to reduce startup latency by deferring non-critical requests and hydrating the UI in stages.
Telegram Mini Apps are usually embedded inside an already constrained environment: they run in an in-app webview, often on mobile networks, and they must initialize quickly enough to feel native. If your app waits for user profile data, feature flags, remote config, and product content before rendering anything, users will experience a blank screen and may leave before the first interaction is possible.
A practical performance win is to separate critical startup work from non-critical work. In Telegram Mini Apps, this usually means:
- Show a minimal shell immediately.
- Read whatever you can from
Telegram.WebAppsynchronously. - Defer expensive or non-essential network requests until after first paint.
- Hydrate the UI in stages instead of blocking on a single “all data loaded” promise.
This approach is especially effective because Mini Apps often need to do at least three things during boot:
- initialize Telegram WebApp APIs,
- fetch backend session or user-specific state,
- render the first meaningful screen.
If those happen sequentially, startup feels slow. If they are carefully prioritized, the app becomes interactive much sooner.
The problem: startup bottlenecks in Mini Apps
A common pattern looks like this:
async function bootstrap() {
const tg = window.Telegram.WebApp;
tg.ready();
const user = await fetch('/api/me').then(r => r.json());
const config = await fetch('/api/config').then(r => r.json());
const catalog = await fetch('/api/catalog').then(r => r.json());
renderApp({ user, config, catalog });
}
bootstrap();
This is simple, but it delays rendering until every request completes. In a Telegram Mini App, that often means:
- longer time to first paint,
- delayed
MainButtonsetup, - poorer perceived performance on slower devices,
- wasted time waiting for data the user may never need.
The key insight is that not all startup data is equally urgent.
Decide what is truly critical
Before changing code, classify your boot-time work.
Critical
These items are required to show the first usable view:
- Telegram init and viewport setup
- a valid user/session identity if the screen depends on it
- route or screen selection
- minimal data needed to render the above-the-fold UI
Non-critical
These can happen after the shell is visible:
- analytics initialization
- AB test assignments that do not affect initial layout
- extra profile fields
- recommendations
- secondary catalogs
- image preloading
- localization bundles for screens not on the current route
If a request does not affect the first screen, defer it.
Pattern: shell first, data second
The most effective optimization is to render a lightweight shell immediately and load the rest progressively.
Step 1: initialize Telegram quickly
Use Telegram-specific state as early as possible. At minimum, call ready() once your app is prepared to show content.
const tg = window.Telegram.WebApp;
tg.ready();
If you use theme parameters, viewport changes, or the back button, set them up early too, but keep that work small.
Step 2: render a skeleton or placeholder view
Do not wait for the entire backend round-trip before painting.
function AppShell() {
return (
<main>
<header className="skeleton header" />
<section className="skeleton card" />
<section className="skeleton card" />
</main>
);
}
A skeleton is usually better than a spinner because it communicates structure and reduces perceived wait time.
Step 3: fetch critical data first
Only fetch the minimum data required for the initial screen.
async function loadCriticalState() {
const res = await fetch('/api/bootstrap', {
headers: {
'X-Telegram-Init-Data': window.Telegram.WebApp.initData,
},
});
if (!res.ok) throw new Error('Bootstrap failed');
return res.json();
}
A dedicated bootstrap endpoint is often faster than multiple round-trips because the server can aggregate the essential pieces in one response.
Step 4: defer the rest
Once the shell is on screen, schedule non-critical fetches.
function defer(fn: () => void) {
if ('requestIdleCallback' in window) {
(window as any).requestIdleCallback(fn);
} else {
setTimeout(fn, 0);
}
}
async function bootstrap() {
const critical = await loadCriticalState();
renderMainView(critical);
defer(() => {
void loadSecondaryData();
void preloadImages();
void initAnalytics();
});
}
This makes the app feel faster without removing functionality.
Use concurrent fetching carefully
Deferment is not the same as serializing everything. Some requests should happen in parallel if they are all critical.
For example, if your startup requires both auth and route data, load them together:
async function bootstrap() {
const tg = window.Telegram.WebApp;
tg.ready();
const [bootstrapData, routeData] = await Promise.all([
fetch('/api/bootstrap').then(r => r.json()),
fetch('/api/route-state').then(r => r.json()),
]);
renderMainView({ ...bootstrapData, ...routeData });
}
Use Promise.all for genuinely parallel critical work, but do not bundle non-critical tasks into the same barrier.
Hydrate by screen, not by app
If your Mini App has multiple tabs, sections, or modals, hydrate only what the user sees first.
For example:
- load the home screen now,
- fetch the settings panel only when opened,
- load item details only when the item is selected,
- initialize heavy charts only when their tab becomes visible.
A useful mental model is progressive hydration:
- HTML/CSS shell and layout
- essential interactivity
- visible data
- hidden or future UI
This is especially important in Telegram Mini Apps because many users navigate with short sessions. You may not get a second chance to make the first screen responsive.
Optimize around Telegram-specific constraints
Telegram Mini Apps have a few characteristics that affect performance decisions.
1. The webview is often resource constrained
Mobile webviews can be more sensitive to long JavaScript tasks than desktop browsers. Keep startup JS small and avoid heavy synchronous work during boot.
2. initData is available immediately
You usually do not need to wait for extra client-side state before sending user identity to your backend. Use initData early, validate it server-side, and bootstrap with one authenticated request.
3. UI chrome changes should be cheap
Operations like setHeaderColor, setBackgroundColor, and button updates are useful, but if you call them repeatedly during startup, you create unnecessary churn. Batch UI chrome updates after you know the initial screen.
4. Viewport events can trigger rerenders
If you listen to viewport or keyboard changes, keep handlers lightweight. Expensive rerenders during startup can negate any gains from deferred data loading.
Example: refactor a slow bootstrap
Here is a before-and-after comparison.
Before
async function bootstrap() {
const tg = window.Telegram.WebApp;
tg.ready();
const user = await fetch('/api/me').then(r => r.json());
const flags = await fetch('/api/flags').then(r => r.json());
const feed = await fetch('/api/feed').then(r => r.json());
const promos = await fetch('/api/promos').then(r => r.json());
renderApp({ user, flags, feed, promos });
}
After
async function bootstrap() {
const tg = window.Telegram.WebApp;
tg.ready();
const critical = await fetch('/api/bootstrap', {
headers: { 'X-Telegram-Init-Data': tg.initData },
}).then(r => r.json());
renderApp(critical);
defer(() => {
void fetch('/api/flags').then(r => r.json()).then(updateFlags);
void fetch('/api/promos').then(r => r.json()).then(updatePromos);
void fetch('/api/feed').then(r => r.json()).then(updateFeed);
});
}
The second version does less blocking work up front and delivers a usable screen sooner.
Measure the right metrics
Optimization is only useful if it improves what users feel.
Track:
- time to first meaningful paint
- time to interactive
- startup JS execution time
- number of requests before first render
- largest contentful element readiness
Inside a Telegram Mini App, also test on:
- lower-end Android devices,
- poor network conditions,
- cold start vs warm start,
- app open from deep link vs standard launch.
A change that helps on desktop but delays mobile paint is not a win.
Practical checklist
Use this checklist when reviewing a Mini App boot flow:
- [ ] Call
Telegram.WebApp.ready()as soon as the shell can be shown - [ ] Render a lightweight placeholder immediately
- [ ] Combine essential backend data into one bootstrap request
- [ ] Separate visible content from background data
- [ ] Defer analytics, prefetching, and secondary features
- [ ] Avoid long synchronous work during startup
- [ ] Hydrate only the current screen first
- [ ] Measure on slow devices and slow networks
Conclusion
For Telegram Mini Apps, the fastest app is not the one that loads everything earliest; it is the one that shows something useful as quickly as possible. The most reliable way to improve startup performance is to shrink the critical path, render a shell immediately, and defer everything that does not affect the first interaction.
If you already have a working Mini App, start by profiling your boot sequence. In many cases, a single refactor—moving non-essential requests out of the startup barrier—can make the app feel dramatically faster without sacrificing features.