To embed a Google map in WordPress without dragging your page speed down, use the free Maps Embed API iframe and add loading="lazy" so the browser defers the embed until the visitor scrolls near it:
<iframe
title="Our office on Google Maps"
width="600"
height="450"
style="border:0;max-width:100%"
loading="lazy"
referrerpolicy="no-referrer-when-downgrade"
allowfullscreen
src="https://www.google.com/maps/embed/v1/place?q=221B+Baker+Street,London&key=YOUR_API_KEY">
</iframe>That is the whole baseline. The place mode of the Embed API takes a URL-escaped address (or place name, plus code, or place ID) in q and your key in key, and renders an interactive map pin. loading="lazy" is the part that protects your performance: an offscreen map iframe is not fetched until the user is about to see it, so a map sitting in your footer costs nothing on first paint.
The rest of this article turns that into something you would actually ship on a real WordPress site: a reusable shortcode that escapes its output, a locked-down API key, and a click-to-load pattern that fires zero Google requests until the visitor opts in.
Why the embed iframe, not the JavaScript API
There are two completely different Google Maps products, and the billing gap between them is the reason this article exists.
- The Maps Embed API (the
/maps/embed/v1/iframe above) has unlimited free usage with no per-load charge. For "show where my business is," it is the correct tool. - The Maps JavaScript API is the pay-as-you-go product. Each dynamic map load is a billable event (roughly $7 per 1,000 loads beyond the monthly free allotment), and it pulls in a much heavier JavaScript payload.
If all you need is a pin on an address, reaching for the JavaScript API is paying real money and real kilobytes for a feature you do not use. Use the Embed API and spend your performance budget elsewhere, the way I argue for on the head-cleanup pass.
A te_map shortcode that escapes its output
Pasting raw iframe HTML into the editor works once. The moment you want the same map in three places, or you let a non-developer place it, you want a shortcode. Here is a small must-use plugin that registers [te_map address="..."] and builds the iframe through esc_url() and esc_attr() so nothing the user types lands in the markup unescaped:
<?php
/**
* Plugin Name: TE Map Embed
* Plugin URI: https://techearl.com/wordpress-lazy-load-google-maps-embed
* Description: A [te_map] shortcode that outputs a lazy-loaded, escaped Google Maps Embed API iframe.
* Version: 1.0.0
* Author: Ishan Karunaratne
* Author URI: https://techearl.com
* License: GPL-2.0-or-later
* Text Domain: te-map-embed
*/
defined( 'ABSPATH' ) || exit;
function te_map_embed( $address, $height = 450 ) {
$key = defined( 'TE_MAPS_EMBED_KEY' ) ? TE_MAPS_EMBED_KEY : '';
if ( '' === $key || '' === trim( (string) $address ) ) {
return '';
}
$src = add_query_arg(
array(
'q' => rawurlencode( $address ),
'key' => $key,
),
'https://www.google.com/maps/embed/v1/place'
);
return sprintf(
'<iframe class="te-map" title="%1$s" loading="lazy" height="%2$d" ' .
'style="border:0;width:100%%;max-width:100%%" ' .
'referrerpolicy="no-referrer-when-downgrade" allowfullscreen src="%3$s"></iframe>',
esc_attr( sprintf( __( 'Map of %s', 'te-map-embed' ), $address ) ),
absint( $height ),
esc_url( $src )
);
}
function te_map_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'address' => '',
'height' => 450,
),
$atts,
'te_map'
);
return te_map_embed( $atts['address'], $atts['height'] );
}
add_shortcode( 'te_map', 'te_map_shortcode' );Drop that at wp-content/mu-plugins/te-map-embed.php and define the key once in wp-config.php:
define( 'TE_MAPS_EMBED_KEY', 'YOUR_API_KEY' );Now an author writes [te_map address="221B Baker Street, London"] and gets a properly escaped, lazy-loaded map. A few details worth calling out:
rawurlencode()on the address turns spaces and commas into%20/%2Cso theqparameter is valid;esc_url()then sanitizes the assembled URL for output.- The key lives in
wp-config.php, not in post content, so swapping it is a one-line change and it never gets copy-pasted into a dozen posts. - The
__()/_e()text-domain calls (te-map-embed) keep the title string translatable, which is just good plugin hygiene.
Lock the API key down with an HTTP-referrer restriction
Here is the part people skip and then panic about: a Maps Embed API key is exposed in your HTML by design. Anyone can view-source, copy your key=..., and use it on their own site. There is no server-side proxy that hides it, because the iframe src is fetched by the visitor's browser, not your server.
The defense is not to hide the key. It is to make a stolen key useless anywhere but your domain. In Google Cloud Console, under APIs & Services → Credentials, edit the key and set Application restrictions → Websites (an HTTP-referrer restriction). Add your domain entries:
https://techearl.com/*
https://www.techearl.com/*Wildcard support is limited but enough: * substitutes for a single subdomain or a path segment, so *.techearl.com/* covers subdomains. With the referrer restriction in place, a request carrying your key but a referrer of attacker.example is rejected by Google with RefererNotAllowedMapError, and it never counts against you. Pair that with an API restriction limiting the key to just the Maps Embed API, so even a leaked key cannot be turned against your billable services.
This is the one non-negotiable step. An unrestricted Maps key on a public page is the kind of thing that turns into a billing surprise on the other Maps products if you ever reuse the key. Restrict it to the embed, restrict it to your hosts, and the exposure stops mattering.
Click-to-load: zero Google requests until the user opts in
loading="lazy" solves performance for maps that are below the fold. It does not solve two other things:
- Privacy / consent. The moment the iframe loads, the visitor's browser talks to Google and ships their IP and referrer. Under GDPR that is a third-party data transfer you may need consent for before it happens, not after.
- Above-the-fold maps. A map at the top of a contact page is in the viewport immediately, so lazy-loading never defers it.
The fix for both is click-to-load: render a cheap placeholder, and only inject the real iframe when the visitor clicks it. No Google request fires until they ask for the map.
<div class="te-map-consent"
data-src="https://www.google.com/maps/embed/v1/place?q=221B+Baker+Street,London&key=YOUR_API_KEY">
<button type="button" class="te-map-load">
Load map (connects to Google)
</button>
</div>A tiny vanilla-JS helper swaps the placeholder for the iframe on click. Note the te-prefixed namespace object, which keeps the helper from colliding with anything else on the page:
const te = {
loadMap(box) {
const src = box.getAttribute("data-src");
if (!src) return;
const frame = document.createElement("iframe");
frame.src = src;
frame.loading = "lazy";
frame.title = "Google map";
frame.allowFullscreen = true;
frame.referrerPolicy = "no-referrer-when-downgrade";
frame.style.cssText = "border:0;width:100%;height:450px";
box.replaceChildren(frame);
},
};
document.addEventListener("click", (event) => {
const button = event.target.closest(".te-map-load");
if (button) te.loadMap(button.closest(".te-map-consent"));
});The placeholder can be a styled <div> (a grey box with the button) or a static map image if you want it to look like a real map. Either way, the third-party connection is deferred until intent is expressed, which is both the privacy-correct behavior and the fastest possible first load. You can wire the same button into a consent-manager so it only appears once the visitor has accepted "maps" in your cookie banner. The same "do not load the third party until the user opts in" thinking applies to analytics, which is why I load Google Tag Manager behind consent rather than on every page view.
Verify it worked
After placing a map, open the page and check three things:
- View-source / Elements: the map
<iframe>carriesloading="lazy", and (for a footer or below-fold map) the browser Network tab shows themaps/embed/v1/placerequest only firing as you scroll toward it, not on initial load. - The key is restricted: temporarily test the embed URL with your key from a different domain (or just trust the Cloud Console "Application restrictions: Websites" entry). A request from a non-allowed referrer returns
RefererNotAllowedMapError. - Click-to-load fires nothing early: with the consent variant, the Network tab should show no request to
google.com/mapsuntil you click the button. If you see one on page load, the placeholder is accidentally rendering the real iframe.
If the map shows a "For development purposes only" watermark or a grey box, the usual causes are an unrestricted-then-mis-restricted key (your domain is not in the Websites list), billing not enabled on the Cloud project, or the Maps Embed API not enabled for that key.
See also
- Clean Up wp_head in WordPress: trimming the default head output so the bytes you save on emoji and oEmbed scripts are not undone by a heavy map embed
- Add Google Tag Manager Without a Plugin: the same consent-first, load-the-third-party-only-when-needed thinking, applied to analytics
- How to Optimize WooCommerce: the larger performance levers on a store, where a deferred map is one small piece of a bigger budget
- Disable WordPress Emojis to Speed Up Your Site: another zero-risk page-weight trim, in the same vein as lazy-loading an embed you do not always need
Sources
Authoritative references this article was fact-checked against.
- Embed a map: Maps Embed API (Google for Developers)developers.google.com
- iframe element (MDN Web Docs): loading attributedeveloper.mozilla.org
- Adding restrictions to API keys (Google Cloud Documentation)docs.cloud.google.com





