Gains: where its trading screen loses time, and what to fix

Measured from the Netherlands in a logged-in Chrome, October 2026.

In short. On a warm reload the screen is trade-ready after 5.6 s (median of 5 loads), 23 of 27 perpetual trading screens, possible rank 19 to 24; after an uncached reload 5.7 s. The largest bottleneck: A 14.2 MB application script. First change to try: Split _app.js by route and move wallet, chain and other-page code out of the trade page's first load.

Three measurements appear on this page, and their times differ. Rank and results come from the campaign's timed reloads. The diagnostic capture and the recorded reload are single loads made with recording switched on, which slows the page: read them for the order of events and the gaps, not for totals.

Resultscampaign: 5 warm and 5 uncached reloads

Warm reloadmedian 5.6 s, range 4.9 s–6.7 s; loads: 5.8 s, 5.3 s, 4.9 s, 6.7 s, 5.6 s
Uncached reloadmedian 5.7 s, range 5.5 s–6.0 s; loads: 6.0 s, 5.7 s, 5.5 s, 5.6 s, 5.7 s
Standing23 of 27 by median; possible rank 19 to 24. 3.1 s behind the fastest, Thalex (2.4 s): 2.3 times as long. The possible rank is the range the measurements leave open: it counts only the exchanges that clearly beat this one and those it clearly beats. Exchanges whose ranges overlap cannot be told apart.
Loads ready5 of 5 warm, 5 of 5 uncached
Notesrecent trades not measured: inactive tab

Where the time goescampaign medians, warm reload

Panels in the order they first showed real, usable data on a warm reload. The coloured part of each row is the time that panel added after the one before it. The last row is trade-ready: every panel is ready and the page has stopped moving.

0 s1 s2 s3 s4 s5 s
Order form2.9 s
Account panel3.0 s +0.2 s
Price3.7 s +0.7 s
Order book3.8 s +0.1 s
Chart4.4 s +0.6 s
Trade-ready5.6 s +1.2 s settling

Stack

Next.js (React); TradingView charting library served from gains.trade; Privy and WalletConnect wallets; Sentry; Intercom.

Delivery

gains.trade through Cloudflare over HTTP/3; HTML and app files carry max-age=300 with stale-while-revalidate=3600, although most file names are content-hashed. On a warm reload a service worker answers 116 of the 277 requests made before trade-ready.

HostRoleCDN edge seenConnectResponse
gains.tradepageCloudflare Amsterdam13 ms12 ms
backend-pricing.eu.gains.trademarket dataCloudflare Amsterdam14 ms13 ms
backend-pricing.gains.trademarket data—— ms— ms

Median of up to 10 attempts from the test machine. Response time includes the server's own time, so it is not a distance.

What happened, in orderdiagnostic capture 1 October, uncached

From one uncached diagnostic load recorded on 1 October 2026 (cache disabled, browser extensions active), so totals are slower than the campaign's timed loads; file names, sizes, headers, order of events and main-thread times are what the findings rely on. A main-thread time given for a script counts all the work that starts in that script, including code it calls in other files, so it shows where start-up work begins, not what one library costs. Endpoint paths were masked when the capture was saved.

WhenWhat
0.03 sHTML arrives from Cloudflare's cache.
0.20–1.23 s_app.js downloads: 3.7 MB compressed, 14.2 MB unpacked, the largest single script in this test. framework.js then runs 1.47 s and _app.js 0.82 s on the main thread.
0.58 sFirst paint.
2.18–2.61 sTwo large JSON responses: backend-global (1.9 MB unpacked, no cache header) and backend-base (1.6 MB unpacked, max-age=0).
2.24 sWalletConnect wallet registry, 1.2 MB unpacked.
2.27 sThe price feed (backend-pricing.eu.gains.trade) opens; handshake done at 3.43 s, 1.15 s later; first message at 3.54 s.
3.67 sThe Privy wallet iframe loads its script chunks (34 requests, 0.94 MB compressed).
5.57 sPrice shows, 2.0 s after the price feed's first message.
5.89 scharting_library.js (0.6 MB compressed, 2.5 MB unpacked) is requested, after the price.
6.80 sChart drawn: trade-ready in this load. 17 long tasks took 2.9 s of main thread.

Recorded warm reload, 3 October

One warm reload recorded with Chrome's performance trace. Recording slows the page, so read the order and the gaps, not the total.

WhenWhat
0.0 sHTML arrives
0.7 sFirst market-data request
2.0 sOrder form ready
2.0 spositions ready
2.0 sAccount panel ready
2.1 sFirst frame sent on a live feed
2.4 sFirst frame received on a live feed
3.4 sPrice ready
3.6 sOrder book ready
4.7 sChart ready
5.4 sTrade-ready

277 requests before trade-ready; 10 long main-thread tasks (over 50 ms) before trade-ready, longest 445 ms; 56 script requests before the first panel; 0 requests over HTTP/1.1.

Request waterfalldiagnostic capture 1 October, uncached

70 of the 229 requests (the page itself, the longest and the largest) started before market data was ready in the 1 October uncached capture, in start order. The outlined part of a bar is waiting for the server; the solid part is downloading. Rows are numbered so a request can be pointed to. The numbers on the right are the size received and the total time. Vertical lines mark when a panel had data in this capture. Shaded rows are named in a finding below, tagged with its ID.

PageStylesheetScriptData requestwaiting for the serverdownloading

0 s0 s1 s1 s2 s2 s3 s3 s4 s4 s5 s5 s6 s6 s1gains.trade9 kB294 ms2gains.trade/d0fac30332df2…155 kB288 ms3gains.trade/webpack-53db2…8 kB392 ms4gains.trade/framework-652…46 kB394 ms5gains.trade/main-e428163a…100 kB402 msGN-16gains.trade/_app-ec873d3a…3.7 MB1027 ms7gains.trade/4023-ddd5280b…39 kB399 ms8gains.trade/trading-77024…29 kB399 ms9gains.trade/_buildManifes…2 kB393 ms10gains.trade/_ssgManifest.…789 B396 ms11sentry.gains.trade348 B1134 ms12sentry.gains.trade348 B1191 ms13gains.trade/8284.c623fefa…187 kB199 ms14backend-global.gains.trade188 kB419 ms15backend-global.gains.trade224 B401 ms16news-forex.gains.trade953 B400 ms17backend-base.gains.trade79 kB431 ms18backend-base.gains.trade210 B398 ms19backend-global.gains.trade228 B396 ms20gains.trade/4727.4ccf5372…816 B627 ms21sdk-cdn.fun.xyz4 kB333 ms22explorer-api.walletconnec…173 kB340 ms23backend-global.gains.trade343 B540 ms24data-api.fun.xyz386 B493 ms25data-api.fun.xyz386 B579 ms26auth.privy.io1 kB479 ms27data-api.fun.xyz383 B549 ms28wrkr.gains.trade512 B1145 ms29mx.gains.trade/surveys.js34 kB375 ms30wrkr.randomdegenerator.xyz543 B759 ms31mainnet.base.org644 B830 ms32gains.trade742 B777 ms33gains.trade/index-46de25e…1 kB436 ms34gains.trade/dashboard-65e…1 kB436 ms35gains.trade/mobile-app-3a…11 kB441 ms36gains.trade/gns-[identifi…1 kB440 ms37mx.gains.trade651 B752 ms38browser-intake-datadoghq.…571 ms39browser-intake-datadoghq.…572 ms40base-mainnet.public.blast…306 B451 ms41gains.trade/6877.2f027385…107 kB112 ms42mainnet.base.org608 B670 ms43auth.privy.io2 kB7 ms44auth.privy.io/2275-fab236…106 kB193 ms45auth.privy.io/7969-9e9cae…108 kB207 ms46mx.gains.trade356 B569 ms47api.moonpay.com683 B380 ms48data-api.fun.xyz385 B378 ms49sentry.gains.trade348 B691 ms50backend-base.gains.trade736 B398 ms51base-mainnet.public.blast…400 B537 ms52mainnet.base.org655 B538 ms53base-mainnet.public.blast…309 B534 ms54mainnet.base.org661 B578 ms55base-mainnet.public.blast…393 B573 ms56mainnet.base.org590 B619 ms57base-mainnet.public.blast…460 B598 ms58mainnet.base.org759 B595 ms59ethereum-rpc.publicnode.c…351 B404 ms60base-mainnet.public.blast…338 B404 ms61api.notifi.network255 B361 ms62data-api.fun.xyz387 B336 ms63mainnet.base.org643 B384 ms64gains.trade/6258.2c06fa71…128 kB76 ms65base-mainnet.public.blast…339 B417 ms66gains.trade796 B287 ms67gains.trade/charting_libr…642 kB294 ms68mainnet.base.org601 B529 ms69base-mainnet.public.blast…382 B363 ms70mainnet.base.org604 B402 msprice 5.6 schart 6.8 s
Download this figure as an image:

Bottlenecks and fixes, largest firstdiagnostic capture 1 October, uncached

GN-1A 14.2 MB application script.grade B
Evidence
_next/static/chunks/pages/_app-ec873d3a6edf5490.js is 3.7 MB compressed and 14.2 MB unpacked, downloaded from 0.20 to 1.23 s. 2.3 s of main-thread work starts in framework.js and _app.js. A 1.0 MB stylesheet (d0fac30332df26e0.css, 155 KB compressed) loads alongside it. On the 3 October warm reload, with the files cached, one task still runs 445 ms at 2.60 s, before the price at 3.42 s.
Gap observed
2.3 s of main-thread work that starts in framework.js and _app.js
Holds back
Start-up as a whole; which panel waits for this work is not established.
Waterfall rows
row 6: 3.7 MB received, 14.2 MB unpacked, public, max-age=300, stale-while-revalid
Fix
Split _app.js by route and move wallet, chain and other-page code out of the trade page's first load.
Where
Next.js app entry (_app) and dynamic imports.
Verify
DevTools → Coverage: unused share of _app.js on the trade page.
GN-2Content-hashed files are cached for 5 minutes, then refreshed in the background.grade B
Evidence
App files, including the 14.2 MB _app.js, the 1.0 MB stylesheet and the fonts, have cache-control "public, max-age=300, stale-while-revalidate=3600": after 5 minutes the browser may still use its copy but has to check it with the server on the next use, and after 65 minutes it must check before using it. charting_library.js (2.5 MB) has the same header and no hash in its name. Privy's files, by contrast, are cached for a year.
Measured
max-age=300 with stale-while-revalidate=3600 for app files and the chart library
Holds back
Not established: the captures do not show which panel waits for this.
Fix
Serve the hashed /_next/static files with max-age=31536000, immutable, and give charting_library.js a versioned file name so it can have the same.
Where
Cloudflare cache rules or the Next.js headers config; the chart library's path.
Verify
DevTools → Network → Headers: cache-control on _app-*.js shows max-age=31536000.
GN-3The price shows 2.0 s after the price feed delivers.grade C
Evidence
1 October capture: pricing socket created at 2.27 s, handshake at 3.43 s, first message at 3.54 s; price on screen at 5.57 s. 3 October recorded warm load: first pricing message at 1.49 s, price at 3.42 s, a gap of 1.9 s again. The captures do not show whether the first message already holds the price of the selected market.
Gap observed
2.03 s between the first price-feed message and the price
Holds back
Price (median 3.7 s in the timed loads).
Fix
Render the price from the first price-feed message; check what the price component waits for after 3.5 s.
Where
Price component and pricing feed client.
Verify
DevTools → Performance: price paint within 200 ms of the first pricing message.
GN-4The chart library is requested only after the price shows.grade C
Evidence
1 October capture: https://gains.trade/charting_library.js (2.5 MB unpacked) is requested at 5.89 s, 0.31 s after the price at 5.57 s; chart drawn at 6.80 s. In all 5 warm campaign loads the chart is the last panel (median 4.4 s).
Gap observed
0.31 s between the price and the chart-library request
Holds back
Chart (median 4.4 s in the timed loads).
Fix
Add a preload link for charting_library.js and its stylesheet on the trade route.
Where
Trade page head or chart loader.
Verify
DevTools → Network: charting_library.js starts before 2 s.
GN-5Wallet code and 3.6 MB of JSON load before the market is on screen.grade C
Evidence
1 October capture: WalletConnect's 1.2 MB wallet list (2.24 s), 31 Privy scripts (3.4 MB unpacked, 0.9 MB compressed, from 3.67 s), and two JSON responses at 2.18 s: 2.0 MB unpacked from backend-global (no cache-control header) and 1.7 MB from backend-base (max-age=0). Both are compressed in transit (188 KB and 79 KB), so the cost is parsing, not download. The timed loads name them as /api/trading-history/24h and /trading-variables/all, matched by host and order.
Measured
a 1.2 MB wallet list, 31 Privy scripts and 3.6 MB of JSON
Holds back
Not established: the captures do not show which panel waits for this.
Fix
Defer the wallet SDKs until a wallet action; trim /api/trading-history/24h and /trading-variables/all to what the first screen needs, or give them a short cache lifetime.
Where
Wallet providers; the backend-global and backend-base endpoints.
Verify
DevTools → Network: no privy or walletconnect requests before the first chart paint; the two JSON responses are well under 1 MB unpacked.
GN-6The public Base RPC rejects requests during start-up (HTTP 429).grade C
Evidence
All 20 older timed loads make 8 to 10 calls to mainnet.base.org before trade-ready. In one uncached load 6 of the 10 are answered with HTTP 429 (too many requests), from 3.8 to 5.6 s, next to 15 calls to a second provider, base-mainnet.public.blastapi.io; its warm counterpart has 3 rejected calls and 12 to the second provider. The 3 October recording has 8 rejected requests before trade-ready. Our test reloaded the page many times within minutes, so a normal visitor may see this less often; the captures do not show whether a rejected call delays a panel.
Measured
6 of 10 calls to the public Base RPC rejected in one load
Holds back
Not established: the captures do not show which panel waits for this.
Fix
Use an RPC provider with a quota for the app instead of the public endpoint, send the chain reads as one batched call, and back off after a 429.
Where
The Base RPC client configuration (mainnet.base.org) and its retry settings.
Verify
DevTools → Network, filter "status-code:429": none during a page load.
Likely saving
Unknown
GN-7Tracking and support requests repeat before the chart.grade B
Evidence
All 20 older timed loads make 5 calls to data-api.fun.xyz/v1/initialize; in the two checked in detail these run (from 2.0 to 4.5 s) and 3 to /v1/rgstr, 7 to Datadog's log intake, 4 to sentry.gains.trade and 2 Intercom pings (the first takes 1.25 s). In the 3 October recording Intercom used 76 ms of main thread.
Measured
21 tracking and support requests before the chart, 5 of them to the same initialise address
Holds back
Not established: the captures do not show which panel waits for this.
Fix
Initialise the fun.xyz SDK once, and send logs, Sentry events and Intercom after trade-ready.
Where
The fun.xyz, Datadog, Sentry and Intercom initialisation code.
Verify
DevTools → Network, filter "fun.xyz/v1/initialize": one request; filter "datadoghq", "intercom": none before the chart is drawn.
Likely saving
Small
GN-8Trade-ready comes 0.7 s after the chart has its data: the chart is still animating.grade B
Evidence
All 10 campaign loads: between the chart having its data and trade-ready the chart panel still has a running animation, for 0.68 to 0.70 s on the uncached loads and 0.70 s or more on the warm ones. One warm load took 2.19 s: the chart also lost its candles for 0.8 s in the middle (trade-ready 6.67 s). Recorded warm load: 0.68 s, all 7 samples.
Gap observed
about 0.7 s between the chart and trade-ready in nine of ten timed loads; 2.2 s in one
Holds back
Trade-ready itself: the page counts as ready only when nothing in a panel is still animating or loading.
Fix
Record a reload with DevTools → Animations and find what still animates in the chart after the candles are drawn, for example a toolbar loading spinner or a fade-in; end it when the data is in.
Where
The chart panel and its loading states.
Verify
DevTools → Animations during a reload: nothing in the chart container animates after the candles appear.
Likely saving
Up to ~0.7 s
GN-9Scripts force the browser to recalculate layout 44 times during a warm reload.grade C
Evidence
3 October recorded warm load: 44 forced layout recalculations costing 120 ms in total; the recording masks the callers.
Measured
44 forced layout recalculations, 120 ms, in the recorded warm load
Holds back
Not established: the captures do not show which panel waits for this.
Fix
Find the code that reads element sizes right after writing to the DOM (DevTools → Performance flags each one as a forced reflow) and batch the reads, in particular during chart and React start-up.
Where
The components that measure themselves while rendering.
Verify
DevTools → Performance on a reload: forced-reflow warnings under 10, total under 30 ms.
Likely saving
Up to ~0.1 s of main thread

Grade B: the capture shows the problem directly. Grade C: the order of events in the capture points to it; the dependency is for the team to confirm. The order is our judgement of each finding's effect on trade-ready, largest first; it is not sorted by gap. A gap is the time between two observed events or the time one piece of work takes; gaps overlap and do not add up. Where a finding is about size or count and no time was measured, the row says what was measured instead. Holds back names the panel the finding delays. Nothing here is graded higher, because no fix was confirmed by changing a page and measuring again.

Savings are estimates from the timeline, not measured after a change. They overlap, so they do not add up; the summary gives the combined estimate.

How this was measured

Timings: 5 warm and 5 uncached reloads in a seeded random order across all exchanges, each judged against a checklist frozen for this page before the campaign; trade-ready is the first moment every checklist panel shows real data, nothing is still loading, and the layout holds still for 0.5 s. The findings on this page come from separate recorded loads, which are never part of the timings. Full method.