dApp Performance Optimization: RPC, Caching, Bundle Size

Slow response and interface freezes under peak load repel users before the first transaction. We optimize dApps end-to-end: we find bottlenecks, configure caching, and restructure requests — the interface responds in fractions of a second, and bundle size is reduced several times over.

Blockchain Development Services

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1376
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1350
  • B2B Advance company logo design
    B2B Advance company logo design
    798
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1088
  • AIDER company logo development
    AIDER company logo development
    1034
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1130

dApp optimization: where the interface loses responsiveness

When a user confirms a transaction in a wallet, the interface should not be thinking. In practice, the button stays inactive for several seconds, the balance updates with a delay, and the list of recent operations is pulled up only after a manual reload.

This is low application responsiveness: formally everything works, but the user does not understand whether their action was applied, and presses again.

The first source of losses is the RPC layer. Each component polls the network on its own: the wallet, balance panel, transaction history, and current gas cost live on their own timers and hit the same node.

Open a dozen tabs or recreate components when navigating between screens — and we get an avalanche of repeated requests to the network: the public node starts responding with failures, and the interface silently shows stale data instead of an error.

The second source is rerendering the entire interface tree on any state update, even when a single balance digit changes; on a weak device this is visible as a jolt during scrolling and input "sticking".

Industry measurements on public Ethereum nodes show the order of magnitude: in a typical dApp, up to half of requests turn out to be duplicate, and the median node response under peak load grows severalfold compared with quiet mode — that is, the bottleneck under load lies not in the blockchain, but in how the interface talks to it.

Let us break down how this is closed: a unified read layer with cache and connection pool, targeted event subscriptions instead of constant polling, and targeted rerendering of only those nodes whose data changed.

What dApp optimization gives: responsiveness, bundle weight, network load

Application responsiveness is determined not by network speed, but by what we load before the first useful screen. We shorten the critical path as follows: first we show data from local state, then we load confirmed data from the chain.

Time to first useful screen on a weak mobile connection drops from several seconds to tenths of a second — the user starts working without waiting for final confirmation of the operation.

Duplicate requests are the main reason an application looks heavy as the number of wallets grows. Deduplication by request key, batching calls into one package, and cache with a lifetime remove most repeats.

The share of duplicate requests falls from tens of percent to single digits, and external nodes stop being a bottleneck even with mass simultaneous entry.

We reduce client bundle weight by route-based splitting and moving heavy modules outside the first screen. The initial bundle shrinks severalfold, and an update does not redownload all code: a returning user gets new data, not a repeat download of the entire application.

A separate measurable effect is the eliminated class of failures during peak activity. Write queues, retries with increasing backoff, polling rate limiting, and a fallback read source remove interface freezing at moments when the number of simultaneous actions rises sharply.

Previously such spikes led to frozen screens and lost transactions; now the load is distributed predictably.

Acceleration options: targeted fixes, cache layer, request restructuring

The three approaches differ not in complexity but in which class of bottlenecks they close. Targeted fixes remove overheating in a specific area, cache and connection pool relieve load from the node, and data layer restructuring changes the very pattern of requests. They are chosen by product stage and by where exactly response time is lost.

Option When suitable Limitations
Targeted performance fixes The bottleneck is localized: extra repeated RPC calls, heavy initial screen loading, growing bundle weight They have a ceiling: structural losses remain, and under load growth the effect dissolves
Data caching and connection pool Many repeated reads and parallel calls to the node, growing audience A cache invalidation policy is needed, otherwise the user sees stale data; writes cannot be accelerated this way
Batch fetching and indexing Dozens of same-type requests per screen, transaction history queries Requires changes in schemas and reindexing, old scenarios have to be rerun
Data layer restructuring Logic assembles data from multiple sources and requests them sequentially Affects contracts between modules, requires parallel run and regression

In practice, the approaches are combined: first remove obvious overheating, then add cache with a clear invalidation policy, and only after that touch the data layer.

This order gives a measurable effect at each iteration and does not require stopping the release of new scenarios.

Workflow: from measuring bottlenecks to documentation handover

A performance audit starts not with fixes but with baseline measurements: without them any optimization becomes guesswork.

We record application behavior in real scenarios — cold start, wallet connection, stream of updates from the network — so that later we compare the result against the baseline, not against feelings.

  1. Baseline measurement. We capture metrics on a test environment replicating production: time until the interface is ready, number of calls to network nodes per screen, amount of code the client loads.
  2. Application profiling. For the client part we use a browser profiler, for the server part a runtime profiler. It shows where time actually goes: repeated reads of the same state, extra rerenders, synchronous operations in the main thread.
  3. Change scheme. We agree on a plan before code: which requests we batch, what we put in cache and for how long, what data we take from the index instead of direct network reads.
  4. Configuration. We implement per the scheme: split client code into chunks with on-demand loading, add retries with increasing backoff for unstable nodes, limit request rate.
  5. Verification on test environment. We run scenarios under load, compare with the baseline, and separately observe degradation when the node is unavailable. If we fall short of target numbers, we return to profiling.
  6. Handover to the team. We provide documentation, configurations, a monitoring dashboard, and a rollback procedure so support does not depend on us.

Each step is closed by an artifact — a report, scheme, or config — so work status is visible at any stage.

Configurations, schemes, and procedures that remain with the team

Work ends not with a report but with a set of artifacts the team uses afterward without us. Each answers its own question: how nodes are configured, where requests go, who has access, when cache is reset, and how dependencies are updated.

  • Node endpoint configuration files: concurrent request limits, timeouts, retries on disconnect, connection pool size — with a note of what load profile each value is set for.
  • Data flow map: what goes to the chain, what is covered by cache, where the boundary lies between them and how it shifts as the audience grows.
  • Access policies: roles and separation of read and write permissions, rotation procedure for service keys, limits per team and external integrations.
  • Cache invalidation rules: lifetimes, binding to new block and contract events, warming hot keys after update, behavior during desync.
  • Client bundle size thresholds: what loads first, what loads on demand, dependency tree, and the point after which the bundle is considered heavier.
  • Dependency update procedure: frequency, compatibility checks on the test circuit, window for reverting to the previous version set.
  • Monitoring and on-call scheme: latency metrics, error and cache hit rates, alert thresholds, degradation response procedure.

Case study: how we eliminated the responsiveness drop under peak load

What was

The application on Polygon had a single network access node, and each screen pulled state independently. At peak — a collection sale, several thousand concurrent sessions — the same data was requested 3–4 times in a row: the share of duplicate requests reached 38%.

Interface response time at the 95th percentile grew from 1.2 to 8 seconds, some calls timed out — the user saw an empty balance, pressed the button again and added load.

What we changed

We moved network access into a separate layer: a provider pool with failover on degradation, coalescing identical calls within one render cycle, and batching small reads into a common request.

State began to be cached by block number, event subscriptions were collected into one connection instead of a stream per component. We removed heavy libraries from the bundle, leaving only the used parts.

What became

Request duplication fell from 38% to 4%, interface response time at the same percentile — to 1.2 seconds, and it does not grow with a tenfold influx. Bundle weight decreased threefold, the first screen stopped waiting for unused code to load.

The class of failures "read state timeout" disappeared completely: during the load test run — not a single one.

How does client-side cache differ from service-side cache?

Client-side cache lives in application memory or local storage and removes repeated calls to the node when returning to an open screen.

Server-side cache sits in front of network nodes and returns one ready response to everyone requesting the same thing — this stops hitting request limits. These are different levels with different error costs, and one cannot be substituted for the other.

Level What it speeds up Where it causes desync What invalidates it
Client-side Rerendering, returning to screen, balance reads Account change, block reorg, network switch Contract events, network and address change, short lifetime
Server-side Frequent same-type reads from many users Lag behind block height, race during writes Block height, request key version, lifetime

Data desync appears where cache outlives a change in network state. The transaction was confirmed, but the application shows the old balance — and the next request goes out with incorrect input data.

The second common case is an account switch in MetaMask: the response for the previous address remains valid by key but incorrect in meaning.

The levels are combined as follows: the client is responsible for rendering speed, the server for saving requests, and the truth always comes from the network.

Cache invalidation relies on events and block height: transaction confirmation resets related keys, and the server layer stores the response together with the block number and does not return it if it has fallen behind. The client layer gets a short lifetime and forced reset on account or network change.

Technical concerns: what is clarified before work starts

Concerns before start are usually the same: the product works, breaking it is scary, and the effect of optimization is not obvious. Below are five questions asked most often, and how we answer them.

The product already works — won't optimization break it?

First measurements, then fixes: we profile scenarios, find the most expensive calls and change them in a targeted way without rewriting working parts.

Rollout goes by traffic percentage, the previous build remains available for rollback, so an unsuccessful change is rolled back in minutes, not overnight.

What if the bottleneck is not in the application at all?

We check the entire chain: network node response time, access point limits, transaction cost and queue.

Quite often the application is not at fault — it is hitting an overloaded node or provider restrictions, and then it is cured by changing access or a dedicated node with a backup.

Won't support complexity increase after changes?

Cache, queues, and background tasks add components that must be maintained. To keep this from becoming a black box, we fix the interaction scheme, data staleness rules, and a set of metrics by which a failure is visible before the user reports it.

What about already deployed contracts and compatibility?

We touch deployed contracts only when explicitly necessary and with a separate migration plan. We check behavior in common wallets, handling of repeated signatures and state discrepancies — the main breakages after optimization happen exactly at this junction.

What happens after work is delivered?

We hand over documentation, schemes, and configurations, go through control points and typical failures with your team.

After that, an agreed support period is possible: fixes, monitoring, and rollback to the previous state at any step until changes prove themselves under real load.

Let's discuss the task: what we need from you for an estimate

For a dApp optimization estimate to be substantive rather than "plus-minus", we need a minimum of inputs.

With them, in one pass we will calculate the effort for reworking hot paths and come back with a plan: what we change in node configuration, where we put cache, which contract calls we move off the frequent path.

  • User scenario: what exactly a person does — wallet login, swap, mint, staking, transaction history — and at which step the drop is currently noticeable.
  • Measurement points: which metrics are collected today — node response time, share of rejected calls, cache hit rate, volume of transmitted data — and how they are collected.
  • Failure points: where exactly the scenario breaks under load — reading state, sending a signature, waiting for confirmation.
  • Timeline constraints: allowed window for service downtime, peak load hours that must not be affected, availability of the test environment for checks.
  • Access to the test environment: test copy of the network and contracts, test keys, restrictions on request limits from the node provider.
  • Stack and current configuration: which cache layers and indexers are already in place, whether there is a dedicated node or we work through external access points.
  • Expected result: what we consider success — target response time, cache hit rate, volume of data transmitted to the client.

With this set, we first reproduce your scenario on the test environment and record baseline measurements, and only then propose changes — otherwise optimization turns into trial and error of hypotheses.

Describe the task — we will come back with a work estimate, stage-by-stage plan, and acceptance criteria.