Standard WooCommerce REST API endpoints trigger full WordPress plugin hooks on every request, resulting in 2–4 second mobile latencies. By combining REST _fields payload pruning, local stale-while-revalidate TTL caching on device, and optimistic cart state in Flutter, we reduce perceived catalog browsing latency to under 300ms—as proven in Warda Al Dava Pharmacy and DN Collection.
- Pruning unused WooCommerce JSON metadata reduces payload transfer sizes by up to 70%.
- Stale-while-revalidate local caching renders known categories in <50ms while refreshing prices silently in the background.
- Your staff keeps using the familiar WordPress/WooCommerce dashboard as the single source of truth.

1. Diagnosing the WordPress / WooCommerce Mobile Bottleneck
WordPress powers over a third of the web, and WooCommerce is the most widely used self-hosted store engine. Merchants like it for good reasons: they own the data, the plugin ecosystem covers almost any need, and their staff already know the dashboard.
The trouble starts when a mobile app is pointed at it. An uncached request to /wp-json/wc/v3/products boots WordPress, loads every active plugin, runs their hooks, queries the database for each product and its metadata, and serialises a very large JSON document. On a store with 30 or more plugins that takes 2 to 4 seconds, and a shopping app makes that request on almost every screen.
The common reaction is to blame WooCommerce and plan a migration. That is rarely necessary. In our Warda Al Dava pharmacy app and the WooStore white-label suite, the store stayed exactly where it was. What changed was how the app asks for data, how long it keeps it, and how it behaves while waiting.
- Ask for less: prune every response to the fields the screen uses.
- Ask less often: cache on the device and refresh in the background.
- Do not wait: update the UI first for actions like add-to-cart, then confirm with the server.
2. Payload Pruning with the `_fields` Parameter
A single WooCommerce product in the default REST response carries dozens of properties: full HTML descriptions, every image size, tax classes, shipping dimensions, upsell IDs and a meta_data array where plugins store whatever they like. A grid of 20 products can easily exceed 200 KB of JSON, of which the grid displays a name, a price and one thumbnail.
The WordPress REST API supports a _fields query parameter that limits the response to the listed properties. A product grid needs about six of them. In our builds, pruning cut payload sizes by up to 70%, which shortens both transfer time on mobile networks and JSON parsing time on the device.
Future<List<ProductSummary>> fetchCategoryPage({
required int categoryId,
required int page,
}) async {
final uri = Uri.https(storeHost, '/wp-json/wc/v3/products', {
'category': '$categoryId',
'page': '$page',
'per_page': '20',
'status': 'publish',
// Six fields instead of sixty.
'_fields': 'id,name,price,regular_price,stock_status,images',
});
final response = await _http.get(uri, headers: _authHeaders);
final rows = jsonDecode(response.body) as List<dynamic>;
return [for (final row in rows) ProductSummary.fromJson(row)];
}The detail screen makes its own request with a longer field list: description, gallery, attributes and variations. Each screen pays only for what it shows. Keep per_page modest and load further pages as the shopper scrolls; asking for 100 products at once just moves the slowness to a different place.
3. Stale-While-Revalidate Caching on the Device
Pruning makes each request cheaper. Caching removes most requests from the shopper's path entirely. The pattern we use is stale-while-revalidate: show what is stored immediately, fetch the current version in the background, and update the screen if anything changed.
Each cached entry has an age. While it is fresh, it is served and no request is made. Once it is stale, it is still served instantly, and a background refresh starts. Only when nothing was ever stored does the shopper wait for the network.
For a returning shopper this means category screens render in under 50ms from local storage, and prices quietly correct themselves a moment later if the merchant changed them.
Stream<List<ProductSummary>> watchCategory(int categoryId) async* {
final key = 'category:$categoryId:page:1';
final cached = await _store.read(key);
if (cached != null) {
yield cached.items; // Instant paint from local storage.
if (cached.age < freshFor) return; // Fresh enough: no request at all.
}
try {
final fresh = await _api.fetchCategoryPage(categoryId: categoryId, page: 1);
await _store.write(key, fresh);
if (cached == null || !listEquals(cached.items, fresh)) {
yield fresh; // Second paint only when something changed.
}
} on NetworkFailure {
if (cached == null) rethrow; // Nothing to show: surface the error.
// Otherwise keep showing cached data with an offline banner.
}
}The same structure gives the app an offline mode for free. With no connection, the shopper can still browse everything they have seen before. The two-tier variant of this cache, with an in-memory layer in front of storage, is described in our shopify_pro_sdk article.
4. Freshness Rules: What May Be Stale and What May Not
Caching a pharmacy catalogue raises a fair question: what if the cached price or stock is wrong? The answer is to be deliberate about which data may be a few minutes old.
- Categories and menus: cache for hours. They change rarely.
- Product lists: fresh for a few minutes, usable while stale for longer.
- Product detail: shown from cache, but price and stock are re-fetched before the item can be added to the cart.
- Cart totals, coupons, shipping rates and payment: always computed by WooCommerce at checkout, never from the device cache.
- Order history: fetched from the network, with the last result kept for offline viewing.
The principle is that the server remains the authority on money. The cache makes browsing fast; it never decides what the customer pays.
5. An Optimistic Cart That Never Makes the Shopper Wait
Add-to-cart is the most important tap in the app. If it shows a spinner for two seconds, some shoppers tap again, some assume it failed, and some leave.
With optimistic state, the cart updates on the device the moment the button is pressed. The badge count changes and the button confirms. The request to the server runs behind that. If it succeeds, nothing more happens. If it fails, the app rolls back the local change and explains why, for example because the item just went out of stock.
The cart is also saved to local storage after every change, so it survives the app being closed, the phone restarting, or the shopper coming back three days later. For a pharmacy, where people reorder the same items, that persistence is what makes a quick reorder possible.
class CartController extends Notifier<Cart> {
@override
Cart build() => ref.read(cartStorageProvider).load();
Future<void> add(ProductSummary product, {int quantity = 1}) async {
final previous = state;
// 1. Update the UI immediately and persist locally.
state = state.withItem(product, quantity);
await ref.read(cartStorageProvider).save(state);
try {
// 2. Confirm stock and price with the server in the background.
await ref.read(cartApiProvider).validateLine(product.id, quantity);
} on OutOfStockFailure {
// 3. Roll back and tell the shopper exactly what happened.
state = previous;
await ref.read(cartStorageProvider).save(previous);
ref.read(messagesProvider.notifier).show('${product.name} just sold out.');
}
}
}6. Images and a Few Server-Side Wins
After JSON, images are the next largest cost. WordPress already generates several sizes of every upload. The app should request the thumbnail size for grids and the large size only on the detail screen, and cache images on disk so a product seen once never downloads again. Loading a 2,000-pixel original into a 160-pixel grid cell wastes data and memory, and on older Android devices it is a frequent cause of scroll jank.
The store itself can also be made quicker without a migration. None of these change how staff work.
- Enable a persistent object cache on the server so repeated product queries do not hit the database each time.
- Use read-only API keys for catalogue requests and keep write permissions for the checkout path only.
- Audit plugins that hook into every REST request; one slow plugin can add a second to every call.
- Serve images through a CDN so thumbnails load from a location near the shopper.
7. Keeping Orders and Stock in Sync Both Ways
Speed is worth nothing if the app and the store disagree. Orders placed in the app are created through the WooCommerce REST API, so they appear in the same Orders screen as website orders, trigger the same emails and reduce the same stock. Staff do not learn a new tool and nothing is reconciled by hand.
In the other direction, WooCommerce webhooks notify our sync layer when an order changes status or a product is updated. The customer gets a push notification when their order is packed or dispatched, and the affected cache entries are marked stale so the next view fetches current data. That is how the pharmacy app keeps inventory fully in step with the website while still reading mostly from cache.
8. Measuring the Result
Performance work should be judged with numbers taken on a real mid-range phone over a mobile connection, not on a developer's laptop on office Wi-Fi. We record three things before and after: time until the first category screen is usable on a cold start, time to open a category the shopper has seen before, and the time from tapping add-to-cart to visible confirmation.
For Warda Al Dava, catalogue browsing went from the multi-second waits of the mobile website to under 300ms in the app, and the build took six weeks from start to launch. The merchant's staff kept using the WordPress dashboard they already knew.
If your WooCommerce store converts well on desktop but poorly on phones, a native app with this architecture is usually a faster and cheaper fix than replatforming. See our Shopify and WooCommerce mobile app service, or send us your store URL and we will tell you what the app would take.
More Engineering Insights
Book a Free Strategy Call
Speak directly with Principal Software Architect Abrar Ali on architecture, timeline, and fixed-scope delivery.
