Images, Fonts and Embeds
The lowest-risk, highest-return group: image loading, hero images, WebP and AVIF variants, local Google Fonts, YouTube previews, lazy rendering, speculative loading, and the WordPress quick toggles. All of it is on the Optimization tab except the lazy-render selectors and Google Maps path exceptions, which are on Exceptions.
Image, font and embed changes are made to the HTML of pages the page cache stores, so they need the page-cache drop-in installed and the origin page cache enabled. Logged-in users and bypassed requests see the page unchanged. Speculative loading and the WordPress quick toggles work through WordPress hooks and apply on every request.
Images
On Optimization > Images and embeds:
| Setting | Key | Default | Range |
|---|---|---|---|
| Lazy-load non-critical images | media.lazy_load | on | |
| Add missing image dimensions | media.add_dimensions | on | |
| Images to load immediately | media.critical_images | 2 | 0 to 10 |
| Hero image rules | media.hero_rules | empty | up to 30 rules |
| Generate optimized variants | media.optimize_uploads | off | |
| Serve optimized image variants | media.rewrite_variants | off | |
| Generated format | media.format | webp | webp, avif |
| Image quality | media.compression | 82 | 30 to 100 |
| Lightweight YouTube previews | media.youtube_previews | off |
MediaOptimizer walks every <img> in document order:
- The hero, when a hero rule finds it:
fetchpriority="high",loading="eager", and optionally a preload link (see below). - The first N images (Images to load immediately, counted from the top of the page):
loading="eager". The first one also getsfetchpriority="high"unless a hero was found or aurl:hero is declared; the rest getfetchpriority="auto". - Every later image:
loading="lazy"when lazy-loading is on. - Every image without a
decodingattribute getsdecoding="async". - Missing dimensions: an image with a
wp-image-<ID>class and nowidthorheightgets both from the attachment’s metadata, which cuts layout shift.
A loading or fetchpriority attribute that is already present is never changed. Your theme, the author, and WordPress core’s own image prioritization have information document order does not, so their decisions win.
Images to load immediately matters more than it looks. Lazy-loading the LCP image hurts the metric you are trying to improve, because the browser delays the one request that decides when the page looks finished. Include your main hero image in the count, or better, name it with a hero rule.
When EWWW Image Optimizer (or Easy IO) already lazy-loads images, adds dimensions, or serves WebP, GT Performance steps back from that feature so two implementations never touch the same image. See Integrations.
To turn lazy-loading off for some pages in code:
add_filter( 'gt_performance_media_lazy_load', function ( bool $lazy ): bool {
return is_front_page() ? false : $lazy;
} );
The filter is applied once per page, not per image. gt_performance_media_add_dimensions works the same way for dimensions.
Hero image rules
Document order is a guess. A hero rule is a statement. One rule per line:
<scope> => <target> [preload]
| Part | Accepted values |
|---|---|
| Scope | * (every page), front_page, post_type:<type>, template:<template> |
| Target | attachment:<ID>, .<class on the img>, or url:<background image URL> |
preload | Optional. Adds a preload link for the hero. |
post_type:product => .wp-post-image preload
front_page => url:https://example.com/wp-content/uploads/hero.jpg
- The first matching rule wins. Up to 30 rules are read, and lines that do not parse are dropped on save.
template:matches a classic page template by its file (template:templates/landing.php) or its name without.php(template:landing), or a block theme template by its slug (template:front-page).attachment:<ID>finds the image by itswp-image-<ID>class or by asrcthat starts with the attachment’s file path, which also matches its resized copies.url:is for a hero drawn as a CSS background. It is always preloaded, and it is never guessed from stylesheets.- With
preload, a<link rel="preload" as="image" fetchpriority="high">is added to<head>, carrying the image’ssrcsetandsizesasimagesrcsetandimagesizesso the browser preloads the right size. It is skipped when the page already preloads that image, or when the hero<img>itself saysloading="lazy".
A single page can override the site rules from the GT Performance box in the editor sidebar. Its Hero image field takes the target part only (attachment:123 preload, .my-hero, or url:…); leave it empty to use the site rules. The value is stored in the _gtperf_hero post meta and applies to newly cached copies of the page. The same box holds the per-page JavaScript options; see JavaScript Optimization.
WebP and AVIF variants
Generate optimized variants creates a WebP or AVIF copy whenever WordPress generates attachment metadata: on upload, or when thumbnails are regenerated. Existing uploads get variants only after their thumbnails are regenerated.
- One background job per unique file (the original and each registered size), so a large import never blocks the upload request.
- JPEG and PNG sources only.
- The variant sits next to its source with the extension swapped:
photo-300x200.jpgbecomesphoto-300x200.webp. An existing variant file is never overwritten. - Encoding uses WordPress’s image editor (Imagick or GD) at Image quality. AVIF needs an image editor with AVIF support on the server.
Serve optimized image variants rewrites src and srcset entries under your uploads URL to the variant, but only where the variant file exists. Everything else keeps its original URL. The original URL is replaced; there is no <picture> fallback.
Filters: gt_performance_generate_image_variants (bool, attachment ID, metadata) and gt_performance_rewrite_image_variants (bool).
Lazy rendering
Exceptions > Media exceptions > Lazy-render selectors (media.lazy_render_selectors) adds one rule to <head>:
<your selectors> { content-visibility: auto; contain-intrinsic-size: auto 800px; }
The browser then skips layout and paint for matching containers while they are off screen. Effective on long pages. Use it on whole sections, never on anything that must be measurable immediately or that an anchor link jumps to. A selector may use letters, digits, spaces, quotes and _ # . - > : ( ) , [ ] =; a selector containing any other character is ignored.
Fonts
On Optimization > Fonts:
| Setting | Key | Default |
|---|---|---|
| Self-host Google Fonts | fonts.self_host_google | off |
| Font display | fonts.font_display | swap (swap, fallback, optional, block) |
Self-hosting works on <link> stylesheets whose host is exactly fonts.googleapis.com (FontOptimizer):
- The first page render that sees one queues a background job and leaves the link unchanged. Nothing is fetched during a visitor’s request.
- The job downloads the stylesheet once (asking for
woff2), then up to 40 font files it names fromfonts.gstatic.com. A file is kept only if its header identifies it as WOFF2, WOFF, OTF or TTF. - Fonts and the rewritten stylesheet are stored in
wp-content/cache/gt-performance/assets/fonts/. - Later renders point the
<link>at the local copy. After that, no visitor request reaches Google, which is the privacy win as much as the performance one.
Google Fonts pulled in by an @import inside another stylesheet or by a JavaScript loader are not localized. See External Services.
Font display is written into each @font-face block of the self-hosted stylesheet when that stylesheet does not already set font-display (a Google Fonts URL with display= in it does). It has no effect on fonts that are not self-hosted. swap shows fallback text straight away; optional may skip the web font on a slow connection; block can leave text invisible for a moment.
The hourly cleanup removes generated font files that nothing has read or modified for 14 days. A stylesheet whose local copy is gone is queued for localization again.
YouTube embeds
Lightweight YouTube previews replaces a YouTube <iframe> (youtube.com/embed/, youtube-nocookie.com/embed/, or youtu.be/) with a 16:9 box showing the video’s thumbnail and YouTube’s red play button. A click anywhere on the thumbnail loads the real player from youtube-nocookie.com with autoplay.
- The embed’s own parameters (start time, captions, and so on) carry over to the player.
- Playlist (
videoseries) and live-stream embeds have no single thumbnail, so they are left as real embeds. - The button covers the whole thumbnail and is labelled with the iframe’s
titlefor screen readers. - The preview’s styles are one stylesheet in the page’s
<head>; only the thumbnail is set inline. Inside a theme with responsive embeds, where core reserves the 16:9 box on the embed block’s wrapper, the preview is pinned over that box the way core pins an iframe, so no empty band appears above it, and a theme’s own embed wrapper can position it too. - Pages cached before 1.1.1 keep the older preview until they are rebuilt.
A standard embed loads YouTube’s player and its third-party connections on every page view, whether or not anyone watches. The preview costs one image. Thumbnails load from i.ytimg.com in the visitor’s browser; your server sends nothing to YouTube.
Speculative loading
WordPress 6.8 and later prefetch a page when a visitor starts to click its link. The Speculative loading card appears only on those versions (speculation.mode):
| Mode | Effect |
|---|---|
| WordPress default, with safety exclusions (recommended) | core, the default. WordPress’s own mode and eagerness, plus GT Performance’s exclusions. |
| Prefetch only when a link is pressed | conservative. Prefetch, conservative eagerness, plus the exclusions. |
| Off | Speculative loading is disabled. |
The exclusions (SpeculationPolicy) are every cache bypass path, which includes the cart, checkout and account paths of every active commerce adapter and your own Paths that must stay dynamic. Prefetching those would start sessions and spend PHP workers on pages that are never cached. With plain permalinks, links carrying action, add-to-cart, remove_item, edd_action, eddfile, download_file, key, token, signature or redirect_to are excluded too. WordPress’s own exclusions (admin, login, files, query-string links) still apply.
When the Speculative Loading feature plugin is active, it keeps control of mode and eagerness and GT Performance adds only its exclusions. See Commerce Safety and Cache Exclusions.
WordPress quick toggles
Switches for things WordPress loads that many sites never use. All off by default.
| Toggle | Effect |
|---|---|
| Disable WordPress emoji assets | Removes the emoji detection script and styles |
| Disable Dashicons for visitors | Dequeues Dashicons for logged-out visitors only |
| Disable WordPress embeds | Removes oEmbed discovery links and the embed host script |
| Disable XML-RPC | Turns off XML-RPC; the REST API stays available |
| Remove RSD link | Removes the Really Simple Discovery link |
| Remove jQuery Migrate for visitors | Drops jQuery Migrate from jQuery’s dependencies on the front end |
| Remove WordPress version | Removes the generator tag and replaces any asset ver= value equal to the WordPress version with a site-specific hash, so cache busting still works after an update |
| Remove shortlink | Removes the shortlink from <head> and response headers |
| Disable every RSS feed | Every feed, including the main one, returns 404 |
| Disable secondary feeds only | /feed/ stays live; comment, category, tag, taxonomy, author, date, search and post type feeds return 404 |
| Remove every RSS feed link | Removes all feed discovery links; feeds keep working |
| Remove secondary RSS feed links | Keeps the main feed link and removes the rest |
| Disable self pingbacks | Stops pings to links on your own site |
| Remove REST API links | Removes REST discovery links; the API keeps working |
| Disable Google Maps | Dequeues registered scripts from maps.googleapis.com or maps.google.com, except on paths listed under Exceptions > WordPress exceptions > Google Maps path exceptions (a fragment of the request path, such as /contact/) |
| Disable password strength meter | Dequeues the strength meter scripts; check sign-up and account forms |
| Remove comment author URLs | Discards comment author website links |
| Add a blank fallback favicon | Prints an empty icon when the site has no Site Icon, saving a failed request |
| Remove global styles | Dequeues global-styles and classic-theme-styles |
| Load separate core block styles | Loads core block CSS only for blocks that render |
Apply site baseline checks the toggles used on gauravtiwari.org (every one above except the two whole-feed switches, the blank favicon, and the two block-style switches) and unchecks the rest. It does not touch the settings under Editor, comments, and APIs. Clear quick toggles unchecks them all. Neither takes effect until you save.
Optimization > Editor, comments, and APIs holds the rest:
| Setting | Default | Notes |
|---|---|---|
| Heartbeat behavior | Reduce frequency | WordPress default, Reduce frequency, Disable outside the editor, or Disable everywhere. Disabling everywhere can break autosave, post locking and some plugins. |
| Heartbeat interval | 60 s | 15 to 120, used by Reduce frequency |
| Autosave interval | 60 s | 15 to 3600. Not applied when AUTOSAVE_INTERVAL is already defined. |
| REST API access | Keep enabled | Administrators only blocks the REST API for everyone else: visitors, and editors and authors using the block editor. Disable all requests blocks it for administrators too, which breaks the block editor. |
| Disable comments | off | Site-wide: closes comments and pings, hides existing comments, removes comment support from every post type, and removes the Comments admin menu |
GT Performance does not limit how many post revisions WordPress keeps; the WP_POST_REVISIONS constant in wp-config.php does that. Deleting existing revisions is covered in Database Cleanup.
Turning a stage off in code
Every HTML stage can be switched off per request with gt_performance_optimize_stage. The stages are css, javascript, media, images (variant rewriting), fonts and embeds (YouTube previews and lazy rendering).
add_filter( 'gt_performance_optimize_stage', function ( bool $allowed, string $stage ): bool {
return 'embeds' === $stage && is_page( 'videos' ) ? false : $allowed;
}, 10, 2 );