Contents
Introduction
This article explains, in exhaustive detail, how to preload WordPress REST API responses on the server and pass them safely to client-side JavaScript using wp_localize_script. You will learn when to do this, how to implement it with best practices for security and performance, how to cache results, how to include nonces for secured REST requests, and how to write the JavaScript consumer that uses preloaded data and falls back to the REST API if needed.
Why preload REST responses?
- Reduce client round-trips: If your front-end immediately needs specific REST data, preloading avoids an extra HTTP request from the browser to your site on page load.
- Improve perceived performance: The first render can happen with all required data available synchronously in your script.
- Better SEO and progressive enhancement: Server-side preloaded data integrated into initial HTML provides a smoother experience for clients that expect immediate content.
- Control and security: You can shape, sanitize, and limit what gets delivered to the client before exposing it to JavaScript.
When not to preload
- When the data is large and only needed after user interaction — do not preload huge payloads on every page load.
- When the data is highly dynamic and must be fresh for every request — prefer on-demand fetches or short cache lifetimes.
- If preloading requires expensive computation, consider caching the result with transients or server caches to avoid slowing page generation.
Conceptual overview
- Server: Make an internal request to the REST API (or call the underlying functions) to build the data structure you want to send.
- Server: Optionally cache or transform that response. Sanitize fields you dont want to expose.
- Server: Register/enqueue the client script and call wp_localize_script to inject the preloaded JSON-object into a global JS variable.
- Client: Read the localized global. Use it immediately if absent or stale, fall back to fetching the REST endpoint using the REST nonce if needed.
Security considerations
- Sanitize and filter: Only send fields the client needs. Remove private fields like internal meta, nonces you don’t want reused, or user password hashes (never present these anywhere).
- Principle of least privilege: Do not include data that changes access control rules or that could be used to escalate privileges.
- Use nonces for write operations: If client code will make write requests (POST/PUT/DELETE), provide a REST nonce with wp_create_nonce(wp_rest) and send it as X-WP-Nonce in fetch headers.
- Escape output where appropriate: wp_localize_script outputs JSON ensure your PHP array contains only data safe to be exposed to logged-out users if the page is public.
Core techniques — server-side
There are two common ways to get REST data in PHP to pass through wp_localize_script:
- Use rest_do_request and the WP REST Server to call internal routes without making an external HTTP call.
- Use wp_remote_get/wp_remote_request to call the REST API over HTTP (works if rest_do_request isn’t available or when calling other URLs).
Method A — Using rest_do_request (recommended for internal routes)
rest_do_request invokes the REST server straight away and avoids creating an HTTP loopback. Example: preload the latest 5 posts from the core posts endpoint and pass them to JavaScript.
// functions.php or plugin file add_action(wp_enqueue_scripts, my_enqueue_with_preload) function my_enqueue_with_preload() { // Register script wp_register_script(my-frontend-script, get_stylesheet_directory_uri() . /js/frontend.js, array(), 1.0, true) // Build a REST request internally request = new WP_REST_Request(GET, /wp/v2/posts) request->set_param(per_page, 5) // Add context if you want full fields: view or edit (edit requires auth) request->set_param(context, view) response = rest_do_request(request) // Convert WP_REST_Response/WP_Error to data array if (is_wp_error(response)) { preload = array(error => true, message => response->get_error_message()) } else { // When rest_do_request returns WP_REST_Response, extract data safely data = response->get_data() // Optionally sanitize specific fields keep only what is required posts = array() foreach (data as item) { posts[] = array( id => (int) item[id], title => wp_strip_all_tags( item[title][rendered] ), excerpt => wp_strip_all_tags( item[excerpt][rendered] ), link => esc_url_raw( item[link] ), date => item[date], ) } preload = array( posts => posts, fetched_at => time(), ) } // Add a REST nonce for future authenticated requests from this client preload[wp_rest_nonce] = wp_create_nonce(wp_rest) // Localize the script wp_localize_script(my-frontend-script, MyPreloaded, preload) // Enqueue script after localizing wp_enqueue_script(my-frontend-script) }
Method B — Using wp_remote_get (loopback HTTP request)
If you prefer or need to call your REST endpoint over HTTP (for example hitting a separate application endpoint or for some caching layer), use wp_remote_get. Be aware of host and authentication concerns on some hosts.
// functions.php or plugin file add_action(wp_enqueue_scripts, my_enqueue_with_remote_preload) function my_enqueue_with_remote_preload() { wp_register_script(my-frontend-script, get_stylesheet_directory_uri() . /js/frontend.js, array(), 1.0, true) url = rest_url(wp/v2/posts?per_page=5context=view) response = wp_remote_get(url) if (is_wp_error(response)) { preload = array(error => true, message => response->get_error_message()) } else { code = wp_remote_retrieve_response_code(response) body = wp_remote_retrieve_body(response) if (code !== 200) { preload = array(error => true, message => REST returned . code) } else { data = json_decode(body, true) posts = array() foreach (data as item) { posts[] = array( id => (int) item[id], title => wp_strip_all_tags( item[title][rendered] ), excerpt => wp_strip_all_tags( item[excerpt][rendered] ), link => esc_url_raw( item[link] ), date => item[date], ) } preload = array( posts => posts, fetched_at => time(), ) } } preload[wp_rest_nonce] = wp_create_nonce(wp_rest) wp_localize_script(my-frontend-script, MyPreloaded, preload) wp_enqueue_script(my-frontend-script) }
Caching preloaded responses with transients
Preloading on every request can be expensive. Use transients to cache JSON-ready arrays for a short period (minutes or seconds depending on freshness requirements).
add_action(wp_enqueue_scripts, my_enqueue_with_cached_preload) function my_enqueue_with_cached_preload() { wp_register_script(my-frontend-script, get_stylesheet_directory_uri() . /js/frontend.js, array(), 1.0, true) transient_key = my_preload_posts_5 preload = get_transient(transient_key) if (preload === false) { // Build internal REST request request = new WP_REST_Request(GET, /wp/v2/posts) request->set_param(per_page, 5) request->set_param(context, view) response = rest_do_request(request) if (!is_wp_error(response)) { data = response->get_data() posts = array() foreach (data as item) { posts[] = array( id => (int) item[id], title => wp_strip_all_tags(item[title][rendered]), excerpt => wp_strip_all_tags(item[excerpt][rendered]), link => esc_url_raw(item[link]), date => item[date], ) } preload = array(posts => posts, fetched_at => time()) // Cache for 5 minutes set_transient(transient_key, preload, 5 MINUTE_IN_SECONDS) } else { preload = array(error => true, message => response->get_error_message()) } } preload[wp_rest_nonce] = wp_create_nonce(wp_rest) wp_localize_script(my-frontend-script, MyPreloaded, preload) wp_enqueue_script(my-frontend-script) }
Enqueue order and wp_localize_script specifics
Important notes when using wp_localize_script:
- Call wp_register_script before wp_localize_script.
- Call wp_localize_script before wp_enqueue_script for the registered handle so localization is printed with the script.
- wp_localize_script expects a JS object name (global) and an array of data. It will output a var MyObject = {…} placed before the script tag.
- wp_localize_script is intended for strings but commonly used to pass arrays/objects. It JSON-encodes the values, so nested arrays are OK.
Client-side consumption
On the client, check the localized object first. If it exists and contains fresh data, use it directly. If it is missing or stale, fetch the REST endpoint (optionally using the nonce returned by the server).
// js/frontend.js (function() { // MyPreloaded is the global object injected by wp_localize_script var preloaded = window.MyPreloaded null // Utility to determine freshness (example: consider fresh if fetched less than 2 minutes ago) function isFresh(preload) { if (!preload !preload.fetched_at) return false var age = Date.now()/1000 - preload.fetched_at return age < 120 } function renderPosts(posts) { var container = document.getElementById(posts-container) if (!container) return container.innerHTML = posts.forEach(function(p) { var a = document.createElement(a) a.href = p.link a.textContent = p.title var li = document.createElement(div) li.appendChild(a) container.appendChild(li) }) } if (preloaded preloaded.posts isFresh(preloaded)) { // Use preloaded posts immediately renderPosts(preloaded.posts) } else { // Fallback: fetch from REST API var headers = {} if (preloaded preloaded.wp_rest_nonce) { headers[X-WP-Nonce] = preloaded.wp_rest_nonce } fetch(window.location.origin /wp-json/wp/v2/posts?per_page=5context=view, { credentials: same-origin, headers: headers }) .then(function(response) { if (!response.ok) throw new Error(Network response was not ok) return response.json() }) .then(function(data) { // Map to the same shape used in preloaded array var posts = data.map(function(item) { return { id: item.id, title: item.title.rendered.replace(/<[^>] >/g, ), excerpt: item.excerpt.rendered.replace(/<[^>] >/g, ), link: item.link, date: item.date } }) renderPosts(posts) }) .catch(function(err) { console.error(Failed to load posts:, err) }) } })()
Using nonces and authenticated endpoints
To make authenticated REST requests from the client, the server should provide a nonce generated with wp_create_nonce(wp_rest). Include that in the localized payload. The client will add it to the request headers as X-WP-Nonce.
// server side preload[wp_rest_nonce] = wp_create_nonce(wp_rest) wp_localize_script(my-frontend-script, MyPreloaded, preload)
// client side fetch with nonce fetch(/wp-json/my-plugin/v1/secret-endpoint, { method: POST, headers: { Content-Type: application/json, X-WP-Nonce: window.MyPreloaded.wp_rest_nonce }, credentials: same-origin, body: JSON.stringify({foo: bar}) })
Preloading custom REST endpoints
If you have a custom endpoint that aggregates complex data, you can register the endpoint and then use rest_do_request to call it internally. Example: custom route that returns a dashboard summary.
// Register a custom endpoint add_action(rest_api_init, function() { register_rest_route(my-plugin/v1, /dashboard, array( methods => GET, callback => my_plugin_dashboard_endpoint, permission_callback => function() { return current_user_can(edit_posts) // as example } )) }) function my_plugin_dashboard_endpoint(request) { // Build expensive aggregated data count_posts = wp_count_posts()->publish recent_posts = get_posts(array(numberposts => 5)) // Format payload data = array( post_count => (int) count_posts, recent => array_map(function(p) { return array(id => p->ID, title => get_the_title(p)) }, recent_posts) ) return rest_ensure_response(data) } // Preload the dashboard at enqueue time add_action(admin_enqueue_scripts, my_admin_enqueue_with_preload) function my_admin_enqueue_with_preload() { wp_register_script(my-admin-script, plugin_dir_url(__FILE__) . admin.js, array(), 1.0, true) request = new WP_REST_Request(GET, /my-plugin/v1/dashboard) // If the endpoint requires permissions, run rest_do_request as the current user during admin_enqueue_scripts (current user exists) response = rest_do_request(request) if (!is_wp_error(response)) { data = response->get_data() // You may want to limit/sanitize fields preload = data } else { preload = array(error => true, message => response->get_error_message()) } wp_localize_script(my-admin-script, MyDashboardPreload, preload) wp_enqueue_script(my-admin-script) }
Advanced considerations
- Multiple endpoints: Merge responses into one object before localizing to avoid multiple localized globals. Example: MyPreloaded = { posts: […], settings: {…}, user: {…} }.
- Avoid overloading: Keep preloaded payloads small (<50-100KB ideally) to avoid making the HTML heavy.
- Compression: Localization output is inlined in the HTML it benefits from GZIP if the page is served compressed, but its still part of the first HTML download.
- ETags and conditional requests: If you need to maintain freshness while minimizing work, consider generating a version or timestamp and only returning data when it changed (server-side cache control or using transients keyed by data version).
- Localization for i18n: Use wp_localize_script to pass strings for translation or use wp_set_script_translations for gettext-ready translations. You can combine both: localize structured data and separately register translations.
- Using wp_add_inline_script instead: For extremely large or deeply nested objects, some developers prefer wp_add_inline_script with wp_json_encode to avoid some legacy quirks of wp_localize_script. But wp_localize_script is sufficient for many use cases and requested specifically here.
Common pitfalls
- Forgetting to register before localize: wp_localize_script requires a registered handle otherwise it fails quietly.
- Exposing sensitive fields: Always inspect the data structure you pull from a REST response before sending it to the client.
- Performance hit: If your preload logic is slow, it will slow down page generation. Use transients, object cache, or only preload when necessary.
- Expecting client to have same capabilities: If your localized object includes edit-context fields, the client may see different results when not authenticated. Be explicit about context when requesting with rest_do_request (context=view or context=edit).
Debugging tips
- Inspect the page source and search for the localized global like var MyPreloaded = … to confirm data is printed.
- Use console.log(window.MyPreloaded) in browser dev tools to see the content available to your script.
- Temporarily reduce data size (per_page=1) to verify everything works end-to-end before scaling up.
- When using rest_do_request, ensure your code runs in a context where current_user and permissions match what the endpoint expects.
Full end-to-end example (concise)
Below is a minimal end-to-end example that demonstrates registering a script, preloading posts with rest_do_request, caching via transient, including a nonce, and localizing the data. This produces an accessible global MyPreloaded on the client.
add_action(wp_enqueue_scripts, example_enqueue_preload) function example_enqueue_preload() { wp_register_script(example-frontend, get_stylesheet_directory_uri() . /js/example.js, array(), 1.0, true) key = example_preload_posts_5 preload = get_transient(key) if (preload === false) { req = new WP_REST_Request(GET, /wp/v2/posts) req->set_param(per_page, 5) req->set_param(context, view) res = rest_do_request(req) if (!is_wp_error(res)) { data = res->get_data() posts = array() foreach (data as item) { posts[] = array( id => (int) item[id], title => wp_strip_all_tags(item[title][rendered]), link => esc_url_raw(item[link]) ) } preload = array(posts => posts, fetched_at => time()) set_transient(key, preload, 2 MINUTE_IN_SECONDS) } else { preload = array(error => true, message => res->get_error_message()) } } preload[wp_rest_nonce] = wp_create_nonce(wp_rest) wp_localize_script(example-frontend, MyPreloaded, preload) wp_enqueue_script(example-frontend) }
// js/example.js (function(){ var data = window.MyPreloaded {} if (data.posts) { // render using preloaded data console.log(Preloaded posts:, data.posts) } else { // fallback to fetch fetch(/wp-json/wp/v2/posts?per_page=5, { credentials: same-origin, headers: data.wp_rest_nonce ? {X-WP-Nonce: data.wp_rest_nonce} : {} }).then(function(r){ return r.json() }) .then(function(json){ console.log(Fetched posts, json) }) .catch(function(){ console.warn(failed to fetch posts) }) } })()
Summary checklist
- Decide whether preloading is appropriate for your data.
- Use rest_do_request for internal calls, wp_remote_get for HTTP loopbacks if needed.
- Sanitize and reduce the payload to only what client code requires.
- Cache preloaded results with transients or object cache to avoid slow page generation.
- Register script first, call wp_localize_script, then enqueue the script.
- Provide wp_rest_nonce when client-side authentication will be necessary.
- On the client, prefer the localized data first and fall back to the REST API when necessary.
Useful links
End of article
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |