Almost every Persian-language article about website speed on Iran's national internet (NIN) tells the story from the wrong side: what has been written about the "national internet" is aimed at users trying to reach sites abroad. This guide is for the other side — site owners whose visitors are in Iran and who keep hearing that their website is slow in Iran or loads half-broken. Below we show why that happens, how to measure it and find the culprit with two repeatable commands, which foreign dependencies to remove and which to leave alone, and exactly which part of the problem an edge node inside Iran fixes and which part it doesn't.
What the "national internet" means for a site owner: a healthy domestic route, a fragile international one
For a webmaster, the "national internet" is not a product. It describes periods when Iran's domestic network, the National Information Network (NIN), keeps working normally while the international gateway is slow, erratic or temporarily closed. The network is not cut off altogether — visitors still reach servers inside Iran and expect your site to work too.
The key point is this: your site doesn't fail as a single unit; it fails piece by piece. The HTML document arrives from a domestic server in a few tens of milliseconds, but if that document points to Google Fonts, a JavaScript library on a foreign CDN and a foreign analytics script, the browser has to cross the fragile gateway for each of them. The result is the familiar "the site loads, but only half of it": the text is there but in a different typeface, the menu won't open, the page stays white for ten seconds. The issue is not "server speed"; it is how many times your page has to cross the border.
Why is your website slow in Iran, or half-broken, on the national internet? Render-blocking and parser-blocking resources
A browser doesn't fetch one file; it fetches an HTML document and reads it to find out what else it needs. Every src and href that points to another host is a new branch, and its cost is not just the download: each new host means one DNS lookup, one TCP connection and one TLS handshake. These branches don't all weigh the same:
- Render-blocking. A <link rel="stylesheet"> to a foreign host holds back the page's first paint; until that file arrives, or its connection fails, the browser draws nothing. This is the long white screen.
- Parser-blocking. A <script> without defer or async stops the browser from reading the rest of the document until the script arrives and runs.
- Non-blocking. Images and iframes (embedded video and maps) stop neither rendering nor parsing, but they leave empty gaps and cause layout shifts — and an iframe holds back the page's load event until it gets a response, so the tab spinner keeps turning and scripts that depend on onload don't run.
And an unpleasant fact: browsers allow timeouts on the order of tens of seconds for a connection that doesn't answer. A foreign host that doesn't answer is worse than a slow one — a slow host answers eventually; a silent one keeps the user behind a white screen. So "the site won't open on the national internet" almost always means a blocking foreign dependency sits on the critical path, not that your server is down.
How to measure TTFB from inside Iran: separating DNS, TCP and TLS
Before changing anything you need a baseline. The most accurate simple tool is curl itself, which reports the time of each phase separately:
# taken on the origin server itself, 2026-08 — a no-network baseline
curl -o /dev/null -sL -w 'dns: %{time_namelookup}\ntcp: %{time_connect}\ntls: %{time_appconnect}\nttfb: %{time_starttransfer}\ntotal: %{time_total}\nbytes: %{size_download}\ncode: %{http_code}\nhops: %{num_redirects}\n' https://mehranhost.cloud/
dns: 0.000719
tcp: 0.000960
tls: 0.026693
ttfb: 0.260044
total: 0.260251
bytes: 141079
code: 200
hops: 1
This output was deliberately taken on the origin server itself, to show the "no-network" baseline: the gap from dns to tcp is under a quarter of a millisecond, which no real customer network will ever give you. Run the same command from a device on a domestic network as well; the difference between the two outputs is the route's share. The -L switch isn't optional either: if hops isn't zero, the domain redirects, and without -L you would only be measuring that 301 response of a few hundred bytes.
The numbers are cumulative, so the time of each phase is the difference between two consecutive lines. A high dns means name resolution took a long time, but your nameservers aren't necessarily to blame: this number measures the path from the device to the resolver, and on a second run it comes from cache and drops to almost zero. To measure the nameservers themselves, run dig @ns1.example.ir example.ir and look at Query time. The gap from tcp to tls is the cost of the cryptographic handshake and depends on network distance. The gap from tls to ttfb is one full network round trip plus the time the server takes to build the response; to separate the two, subtract the dns-to-tcp gap — which is almost a pure round trip — from it, and what remains is the share of your application and database. With an origin outside Iran, a large part of this number is the international gateway itself, and optimizing queries won't move it. Finally, the gap from ttfb to total depends both on the size of your HTML and on the route's effective bandwidth. We used the same tool in our guide to reducing ping to your server from Iran to measure route latency; here the question is where the time goes.
The thresholds of the Mehran Host website speed test are a good starting point for judging the numbers: TTFB under 400 ms passes, 400 to 800 ms is a warning, over 800 ms fails; TCP plus TLS under 250 ms passes and over 500 ms fails; a DNS answer under 150 ms passes and over 400 ms fails; a total page weight under 1.5 MB passes and over 3 MB fails; up to 45 requests passes and over 90 fails. Two honest caveats. First, curl measures your own connection, not the experience of every user. Second, no online tool — ours included — measures from your users' home networks, because it sends the request from its own server. So the division of labor is clear: take the page's structural numbers — weight, request count, CDN presence, SSL status — from the tool, and the network-route numbers from curl, run on the network that matters to you. To learn which networks your real users are on, see our guide to website visitor analytics.
How to find a website's external resources: list the hosts with one command
The usual advice is to open the Network tab of the developer tools, but that view is polluted by your browser cache, your extensions and the state of your connection at that moment. What you can hand to someone else is a repeatable command that counts the raw HTML (tested on Linux with GNU grep and sed; on macOS, install ggrep and gsed or run it on the server itself):
curl -sL --max-time 25 https://mehranhost.cloud/ \
| grep -oiE '(src|href)=["'"'"'](https?:)?//[^"'"'"']+' \
| grep -oiE '(https?:)?//[^/"'"'"']+' \
| tr 'A-Z' 'a-z' \
| sed -E 's#^(https?:)?//##' \
| sort | uniq -c | sort -rn
66 www.mehranhost.cloud
1 www.googletagmanager.com
1 trustseal.enamad.ir
The output above is a real measurement of this site's home page in late August 2026: 66 references to our own domain and two third-party hosts — Google Tag Manager, which really is foreign, and the eNamad trust seal (the official Iranian e-commerce trust badge), which lives on trustseal.enamad.ir inside Iran and has to load from its own domain. On an untouched WordPress site the same command usually returns four to seven lines, with fonts.googleapis.com, cdn.jsdelivr.net and secure.gravatar.com among them. We won't pretend that zero is easy to reach; the point is that every line you keep is there on purpose.
Every line that isn't your own domain is a third-party branch. Now set aside the .ir domains and Iranian hosts — whatever is left crosses the border. The details of the command are deliberate too: (https?:)?// catches protocol-relative URLs such as //fonts.googleapis.com, which are common in WordPress themes; the character class with both quote marks also sees single-quoted attributes like src='…'; and tr copes with uppercase letters. In our test, the simpler version that circulates widely found only one of four foreign hosts.
There is also a hidden layer this command can't see: references inside CSS files, such as an @import of Google Fonts or a url() pointing to an image on another host. Repeat the same method one layer down:
curl -sL https://example.ir/wp-content/themes/mytheme/style.css \
| grep -oiE '(@import|url\()[^;)]*(https?:)?//[^"'"'"')]+' | sort -u
To be honest about the limits: these commands only see what is written in the raw HTML and CSS. If a script injects a new host at runtime — a tag manager, a chat widget, an ad — it won't show up in the list, and you need a real browser to see it.
8 foreign dependencies almost every Iranian website has
Your list is probably a subset of these eight. The middle column matters most, because the symptom alone lets you guess which dependency is to blame, without any tool:
| Foreign dependency | Symptom on the national internet | Local replacement |
|---|---|---|
| Google Fonts via link or @import | The page stays white for a few seconds, then the text appears in a system font and the layout jumps | A subsetted woff2 hosted on your own domain with font-display: swap |
| A JavaScript library from a foreign CDN | Sliders, the mobile menu and forms stay unresponsive | A pinned version on your own server, with a versioned file name |
| A foreign icon font | Squares or random letters appear instead of icons | Inline SVG for the icons you actually use |
| Gravatar avatars in comments | The comments section finishes late, and rows jump as the images arrive | A local default avatar, or uploads to your own hosting |
| Embedded video or map | An empty rectangle that never fills in | An Iranian video service; for a map, a static image with a link |
| Foreign analytics script and tag manager | The page takes long to finish, and visits are under-counted | Analytics running on the same hosting, or an Iranian analytics service |
| Foreign comment or ad widget | A block that never fills and shifts the page height | The CMS's own comment system |
| Images hosted on a foreign service | Missing images, especially in old copied content | Move the files to your own server and rewrite the URLs |
The first three rows break the page's look and function and are urgent; the rest affect how long the page takes to finish loading and can wait their turn.
Removing Google Fonts from WordPress: self-host your Persian font on your own domain
The Persian font is almost always the heaviest dependency of an Iranian website: it is large, and loaded with a link it becomes render-blocking.
1 — woff2 only, and only the subset you need
According to web.dev's guide to web font best practices, WOFF2 compresses about 30 percent better than WOFF, and today's browsers need no other format. The next step is subsetting, because Persian fonts usually also carry extended Latin, Greek, Cyrillic and dozens of OpenType features you will never use. The standard tool for this is pyftsubset from the fontTools package:
pyftsubset Vazirmatn.woff2 \
--output-file=Vazirmatn-fa.woff2 --flavor=woff2 \
--layout-features=ccmp,init,medi,fina,isol,rlig,liga,calt,locl,mark,mkmk,curs,kern \
--unicodes="U+0020-007E,U+00A0,U+00AB,U+00BB,U+00D7,U+0600-06FF,U+200C-200F,U+2010-2015,U+2018-201D,U+2022,U+2026,U+2212,U+FB50-FDFF,U+FE70-FEFF"
Take two details of this range seriously. Persian guillemets (« »), the em dash (—) and the ellipsis (…) are not in the Arabic block, and they get left out of the short lists that are passed around the Persian web; drop them and your Persian text renders with empty boxes. And we have kept the whole U+0600-06FF block, not just U+0621 onward, so that text typed in by users survives too.
The result on this site with fontTools 4.27.1, in late August 2026: Vazirmatn went from 109 to 61 KB — 43 percent smaller — and the four fonts the pages need went from 188 to 126 KB in total, about 62 KB less on every first visit. The site's real build also keeps every character the pages render, so the command above comes out slightly smaller (59 KB). The number that matters is the percentage, not the exact bytes.
2 — Declare the font in CSS with font-display: swap
According to MDN's font-display documentation, the swap value gives "an extremely small block period and an infinite swap period": the browser shows the text almost immediately in a fallback font and swaps in the real font whenever it arrives. On a slow network, this one line is the difference between "I can read the text" and "the page is white".
@font-face {
font-family: "Vazirmatn";
src: url("/assets/fonts/Vazirmatn-fa.woff2") format("woff2");
font-weight: 100 900; /* one variable file instead of five static ones */
font-display: swap;
}
3 — Preload, and only one weight
The browser doesn't know it needs a font until it has read the CSS, and preload removes that delay. A font file is requested in CORS mode even from your own domain, so the crossorigin attribute is mandatory — without it the browser downloads the file twice. And preload only one weight, because a preload takes priority away from other resources.
<link rel="preload" href="/assets/fonts/Vazirmatn-fa.woff2"
as="font" type="font/woff2" crossorigin>
Now the honest part, because plenty of wrong claims are made about this: if the web font doesn't arrive, the browser falls back to a system font and the Persian text still renders correctly and legibly — the zero-width non-joiner stays in place and the letters join, because that is the job of the operating system's text-shaping engine, not of your font. What really breaks is the letter widths: line lengths change, the page jumps noticeably the moment the real font arrives, and the Persian typography is lost.
Scripts, icons and libraries: from a foreign CDN to your own server
The pattern is the same for every library. First find the exact version you are using right now — not "the latest version", because you shouldn't upgrade and move at the same time. Then put the file in a directory such as /assets/vendor/ under a versioned name, and change the URL. And the step everyone forgets: check the delivery headers.
curl -sI --compressed https://example.ir/assets/vendor/lib-3.7.1.min.js | grep -iE 'HTTP/|cache-control|content-encoding|content-length'
HTTP/2 200
cache-control: public, max-age=31536000, immutable
content-encoding: br
content-length: 31544
The --compressed switch is essential: without it, curl sends no Accept-Encoding header and content-encoding never appears in the output — so even on a server where Brotli works perfectly you would wrongly conclude that "compression is off". On one file of this site, without the switch you only see content-length: 11006; with it, you see content-encoding: br and content-length: 3418. The rest is easy to read: a short cache-control means you have only shortened the distance and still pay the download cost on every visit; and if the encoding is still empty with --compressed, compression really is off. Because the file name is versioned, a one-year cache lifetime is safe.
For icons there is a better way. Most sites use only ten to fifteen icons out of an icon font that weighs several hundred kilobytes; put just those icons in the template as inline SVG, and the dependency and its request disappear together. In WordPress these URLs usually come from the theme or a plugin, and the proper fix is to register the same resource again with a local URL — we walk through it in our guide to speeding up WordPress.
What you should not self-host
Removing foreign dependencies isn't an absolute goal. Some services must load from their own origin, and copying them either doesn't work or is dangerous.
- reCAPTCHA and similar anti-bot services. The security model of these tools rests on the browser talking directly to the provider's server, so a local copy of the script makes no sense. There is, however, an official and little-known fix: the reCAPTCHA FAQ says to use www.recaptcha.net in your code when www.google.com is not accessible. The fix is changing the host, not self-hosting — and it isn't complete: the api.js file served from recaptcha.net still references www.gstatic.com, as you can see by running the command from the previous section on that file. If forms still don't work on the national internet, move to a self-hosted CAPTCHA or an Iranian anti-bot service. And one note for 2026: according to Google Cloud's guide to migrating classic reCAPTCHA, the old keys have moved to Google Cloud and the free tier is capped at 10,000 assessments a month.
- Payment gateway scripts. Any code on the transaction path must come directly from the provider. The good news is that Iranian payment gateways are domestic and don't cross the international gateway.
- Anything whose license forbids redistribution — commercial maps, fonts with restrictive licenses, and APIs that need a key.
- Anything you can't keep up to date. If you have no update process for a library, the provider's hosted version is safer than your forgotten copy.
For whatever remains, lazy-load it instead of removing it: replace videos and maps with a light static image, and put off the real loading until the user clicks.
Page weight and cache headers: how to cut size and request count
A common mental error is to blame all the slowness on distance. But when the international gateway is under pressure, it is not only latency that rises; the effective bandwidth of each connection falls too, and a four-megabyte page won't give a good experience even from the nearest possible node. Cutting bytes is worth as much as shortening the route — and unlike cutting TTFB, which is tied to the route and the server, it is entirely in your hands.
Three levers, in order of payoff: images, which make up the largest share, and converting them to WebP or AVIF often removes half of the page's weight; text compression with Brotli, or at least gzip, which is a one-time setting on the web server; and cache headers, which make the second visit almost free — versioned static files with max-age=31536000, immutable, and the HTML itself with a short lifetime. The exact meaning of each header is in MDN's HTTP caching documentation.
And a fourth lever: the number of requests. Every separate file has its own cost even on HTTP/2, and on an erratic network that cost multiplies. A page with more than 90 requests is almost always worth a line-by-line review; in WordPress most of those requests usually come from the theme and plugins loading their files on every page, even where they aren't used. Defer images further down the page with loading="lazy" — but never the main image at the top of the page.
What a CDN with an Iran edge does and doesn't do
Once the foreign dependencies are gone, putting an edge node inside Iran in front of the site starts to make sense. The domain's public record points to the edge nodes instead of the origin server's IP, and each visitor's request reaches the nearest node first — we explain the HIT and MISS mechanics in What is a CDN?. What matters here is where this approach stops working.
What the edge node inside Iran does: if your origin is inside Iran and the Iran node is within the site's scope, all cached static content — images, CSS, JavaScript, fonts — is served to visitors in Iran from inside the country. If the origin is abroad, the same content comes from the nearest international node, which is still closer than the origin. Either way, the origin server's IP stays hidden and the edge can apply a request-rate limit.
And what the edge node doesn't do: it doesn't make your page's foreign dependencies local. If your theme still points to Google Fonts, the user's browser goes straight to Google, and your CDN isn't on that path. If the origin server is outside Iran, every uncached response — nearly all personalized HTML and every dynamic action — is still a full round trip across the international gateway. And edge caching isn't something you switch on yourself: HTML is already cached for a short time and static files for a long time, and the only setting in your hands is the "Cache level" — Standard, Aggressive with six-hour HTML caching, or No cache. So the order of work matters: remove the dependencies first, then put the node in front.
In the Mehran Host panel this is done with a switch next to each A record, and the node scope can be chosen. On "Automatic", the system checks where the origin server is and decides on that basis: with an origin inside Iran, all nodes are enabled; with an origin abroad — or when the measurement is inconclusive — only the international nodes, which is the safe default. You can also set either one by hand, but the panel's all-nodes option, "All, including Iran", only pays off with an origin inside Iran, for the reason explained in the next section. If A records are new to you, see our guide to DNS records.
Where should the origin server be? Iran hosting or foreign hosting
An edge node makes the origin's location less important but doesn't make it irrelevant, because every uncached response goes straight to the origin. The decision rule is a number, not a matter of taste: pull the share of uncacheable requests from the server log — personalized HTML, the shopping cart, the dashboard, search. The bigger that share, the more the origin's location decides the outcome, and the less the edge node can take off your hands. If most of your visitors are inside Iran and your service has no need to stay abroad, an origin inside Iran makes every uncached round trip domestic too; the numerical criteria for this choice are in our comparison of Iran hosting and foreign hosting.
But some workloads really do have to stay abroad: a service that depends on foreign APIs, or a site whose main audience is outside the country. Here we have to be blunt, because the common advice is wrong: putting an Iran node in front of a foreign origin doesn't help, and it usually comes out slower, because it sends the Iranian visitor to a node inside the country and from there back across the border — which is why, on "Automatic" with a foreign origin, the Iran node is deliberately left out. The real fix is this: the origin stays where it is, the site goes behind the international nodes, and all the effort goes into removing the page's foreign dependencies and cutting its weight. If you want Iranian visitors to avoid crossing the border even for the text itself, the way to do that is to bring the origin into Iran. How traffic between the origin and the edge is counted is covered in our VPS bandwidth guide.
A 10-step checklist for speeding up your site on the national internet
The checklist below is everything you need to speed up a website on Iran's national internet. Its order is deliberate — from the cheapest, highest-impact steps to the infrastructure decisions.
- Take a baseline: run the curl timing command from a domestic network and save the six numbers.
- List the page's foreign hosts with grep, and note how many there are.
- Check your main CSS files separately for foreign @import and url() references.
- Self-host the font, subset it, convert it to woff2, and keep the Arabic-script shaping features.
- Preload one font weight with crossorigin, and declare font-display: swap.
- Bring JavaScript libraries onto your own server as pinned versions.
- Replace the icon font with inline SVG.
- Lazy-load the remaining videos, maps and foreign widgets.
- Check compression and cache headers, and move images to WebP or AVIF.
- Put an edge node inside Iran in front of the site, and decide where the origin should live.
And the last step, which was really the first: take the same three numbers again. If the foreign hosts have gone from seven to two and the page weight has halved, the job was done right; if nothing has moved, any claim of "improved speed" is just a feeling.
Mehran Host's tools and CDN
Our free website speed test, which also checks your site's health, needs no sign-up and lets guests run three checks per domain per day, plus a daily cap for each connection; signing in to an account removes the per-domain limit. The measurement runs from the service's own server — not from your users' networks — so use it for the page's structural numbers and measure the network route separately with curl. The report breaks TTFB down into DNS, TCP, TLS and server processing time, counts the requests, and estimates total page weight from a sample of the page's files — and says so plainly in the report with an "estimated" label. It also detects a CDN, shows the host's IP and ASN and the SSL status, and gives a score from 0 to 100 across four categories — speed, optimization, security, and "quality and SEO" — weighted 40, 25, 20 and 15 percent, using the same thresholds quoted above.
Mehran Host's Iran CDN has its nodes in Iran and Europe, turns on with a switch next to the A record, hides the origin IP, issues SSL automatically, supports Brotli, HTTP/2 and HTTP/3, applies a request-rate limit, shows statistics for each node separately, and offers JPEG and PNG conversion to WebP and AVIF on the edge — an option you enable per site, which doesn't apply in "No cache" mode. It works on domains whose DNS is hosted with Mehran Host, and each account's first 1 TB of CDN traffic is free; if you want to bring your origin into Iran, the Iran web hosting page shows the options. But we repeat the point from the CDN section: none of this removes the Google Fonts line from your theme. That is the first step, and it is yours.
Frequently asked questions
Why is my website slow in Iran on the national internet when it is fast otherwise?
Your website slows down on the national internet because the page points to hosts outside Iran, and the browser has to cross the international gateway for each one; the most common are Google Fonts, JavaScript libraries on foreign CDNs, icon fonts and analytics scripts. Your own server may be healthy and inside Iran; what slows down is the page's foreign branches, which you can find with curl and grep on the raw HTML.
How do I remove Google Fonts from WordPress, and what can it break?
Removing Google Fonts from WordPress takes four steps: put the font's woff2 file on your own server, subset it to the Persian range with pyftsubset, declare it in CSS with font-display: swap, and remove the Google Fonts URL. If the font doesn't arrive, the browser falls back to a system font and Persian text still renders correctly with joined letters; what breaks is the typographic proportions, plus a layout shift. The most common way to break letter joining is dropping the Arabic-script shaping features while subsetting.
How do I test my website's speed from inside Iran?
The most reliable way to test your website's speed from inside Iran is to run curl with a custom output format from a device on that same network — not from the server itself — because it reports DNS, TCP, TLS and first-byte times separately. Online tools, including the Mehran Host website speed test, send the request from their own servers and can't see your network path; instead they give page weight, request count, CDN detection and SSL status, which don't depend on the network.
Does a CDN with an Iran edge completely fix slowness on the national internet?
No, a CDN with an Iran edge does not completely fix slowness on the national internet. An edge node inside Iran delivers cached static content from inside the country — provided the origin server is inside Iran as well — but it doesn't make the page's foreign dependencies local: if your theme points to Google Fonts, the user's browser goes straight to Google, and the CDN isn't on that path. The right order: remove the dependencies first, then add the edge node.
My website won't open at all on the national internet. Where do I start?
A website that won't open at all on the national internet most likely has a blocking foreign dependency on its critical path — usually a CSS file, or a script without defer, on a host that doesn't respond; the browser waits for tens of seconds and the user sees a white page. First check with curl whether your own server responds, then list the foreign hosts and remove the blocking references.