{"id":900,"date":"2015-04-30T16:11:58","date_gmt":"2015-04-30T20:11:58","guid":{"rendered":"http:\/\/www.visophyte.org\/blog\/?p=900"},"modified":"2015-04-30T17:39:26","modified_gmt":"2015-04-30T21:39:26","slug":"talk-script-firefox-os-email-performance-strategies","status":"publish","type":"post","link":"https:\/\/www.visophyte.org\/blog\/2015\/04\/30\/talk-script-firefox-os-email-performance-strategies\/","title":{"rendered":"Talk Script: Firefox OS Email Performance Strategies"},"content":{"rendered":"<p>Last week I gave a talk at the <a href=\"http:\/\/2015.phillytechweek.com\/events\/dev_day_big_talks\">Philly Tech Week 2015 Dev Day<\/a> organized by the delightful people at <a href=\"http:\/\/technical.ly\/\">technical.ly<\/a> on some of the tricks\/strategies we use in the Firefox OS Gaia Email app.\u00a0 Note that the credit for implementing most of these techniques goes to the owner of the Email app&#8217;s front-end, <a href=\"http:\/\/jrburke.com\/\">James Burke<\/a>.\u00a0 Also, a special shout-out to <a href=\"https:\/\/github.com\/vingtetun\">Vivien<\/a> for the initial DOM Worker patches for the email app.<\/p>\n<p>I tried to avoid having slides that both I would be reading aloud as the audience read silently, so instead of slides to share, I have the talk script.\u00a0 Well, I also have the <a href=\"https:\/\/clicky.visophyte.org\/files\/presentations\/\">slides here<\/a>, but there&#8217;s not much to them.\u00a0 The headings below are the content of the slides, except for the one time I inline some code.\u00a0 Note that the live presentation must have differed slightly, because I&#8217;m sure I&#8217;m much more witty and clever in person than this script would make it seem&#8230;<\/p>\n<p><b>Cover Slide: Who!<\/b><\/p>\n<p>Hi, my name is Andrew Sutherland. \u00a0I work at Mozilla on the Firefox OS Email Application. \u00a0I\u2019m here to share some strategies we used to make our HTML5 app Seem faster and sometimes actually Be faster.<\/p>\n<p><b>What\u2019s A Firefox OS (Screenshot Slide)<\/b><\/p>\n<p>But first: What is a Firefox OS? \u00a0It\u2019s a multiprocess Firefox gecko engine on an android linux kernel where all the apps including the system UI are implemented using HTML5, CSS, and JavaScript. \u00a0All the apps use some combination of standard web APIs and APIs that we hope to standardize in some form.<\/p>\n<p><a href=\"http:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-medium wp-image-901\" src=\"http:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-1-169x300.png\" alt=\"Firefox OS homescreen screenshot\" width=\"169\" height=\"300\" srcset=\"https:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-1-169x300.png 169w, https:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-1.png 320w\" sizes=\"auto, (max-width: 169px) 100vw, 169px\" \/><\/a><a href=\"http:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-2.png\"> <img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-medium wp-image-902\" src=\"http:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-2-169x300.png\" alt=\"Firefox OS clock app screenshot\" width=\"169\" height=\"300\" srcset=\"https:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-2-169x300.png 169w, https:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-2.png 320w\" sizes=\"auto, (max-width: 169px) 100vw, 169px\" \/><\/a> <a href=\"http:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-3.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-medium wp-image-903\" src=\"http:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-3-169x300.png\" alt=\"Firefox OS email app screenshot\" width=\"169\" height=\"300\" srcset=\"https:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-3-169x300.png 169w, https:\/\/www.visophyte.org\/blog\/wp-content\/uploads\/2015\/04\/screenshot-3.png 320w\" sizes=\"auto, (max-width: 169px) 100vw, 169px\" \/><\/a><\/p>\n<p>Here are some screenshots. \u00a0We\u2019ve got the default home screen app, the clock app, and of course, the email app.<\/p>\n<p>It\u2019s an entirely client-side offline email application, supporting IMAP4, POP3, and ActiveSync. \u00a0The goal, like all Firefox OS apps shipped with the phone, is to give native apps on other platforms a run for their money.<\/p>\n<p>And that begins with starting up fast.<\/p>\n<p><b>Fast Startup: The Problems<\/b><\/p>\n<p>But that\u2019s frequently easier said than done. \u00a0Slow-loading websites are still very much a thing.<\/p>\n<p>The good news for the email application is that a slow network isn\u2019t one of its problems. \u00a0It\u2019s pre-loaded on the phone. \u00a0And even if it wasn\u2019t, because of the security implications of the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/TCPSocket\">TCP Web API<\/a> and the difficulty of explaining this risk to users in a way they won\u2019t just click through, any TCP-using app needs to be a cryptographically signed zip file approved by a marketplace. \u00a0So we do load directly from flash.<\/p>\n<p>However, it\u2019s not like flash on cellphones is equivalent to an infinitely fast, zero-latency network connection. \u00a0And even if it was, in a naive app you\u2019d still try and load all of your HTML, CSS, and JavaScript at the same time because the HTML file would reference them all. \u00a0And that adds up.<\/p>\n<p>It adds up in the form of event loop activity and competition with other threads and processes. \u00a0With the exception of Promises which get their own micro-task queue fast-lane, the web execution model is the same as all other UI event loops; events get scheduled and then executed in the same order they are scheduled. \u00a0Loading data from an asynchronous API like <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/IndexedDB_API\">IndexedDB<\/a> means that your read result gets in line behind everything else that\u2019s scheduled. \u00a0And in the case of the bulk of shipped Firefox OS devices, we only have a single processor core so the thread and process contention do come into play.<\/p>\n<p>So we try not to be a naive.<\/p>\n<p><b>Seeming Fast at Startup: The HTML Cache<\/b><\/p>\n<p>If we\u2019re going to optimize startup, it\u2019s good to start with what the user sees. \u00a0Once an account exists for the email app, at startup we display the default account\u2019s inbox folder.<\/p>\n<p>What is the least amount of work that we can do to show that? \u00a0Cache a screenshot of the Inbox. \u00a0The problem with that, of course, is that a static screenshot is indistinguishable from an unresponsive application.<\/p>\n<p>So we did the next best thing, (which is) we cache the actual HTML we display. \u00a0At startup we load a minimal HTML file, our concatenated CSS, and just enough Javascript to figure out if we should use the HTML cache and then actually use it if appropriate. \u00a0It\u2019s not always appropriate, like if our application is being triggered to display a compose UI or from a new mail notification that wants to show a specific message or a different folder. \u00a0But this is a decision we can make synchronously so it doesn\u2019t slow us down.<\/p>\n<p><b>Local Storage: Okay in small doses<\/b><\/p>\n<p>We implement this by storing the HTML in <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/localStorage\">localStorage<\/a>.<\/p>\n<p>Important Disclaimer! \u00a0LocalStorage is a bad API. \u00a0It\u2019s a bad API because it\u2019s synchronous. \u00a0You can read any value stored in it at any time, without waiting for a callback. \u00a0Which means if the data is not in memory the browser needs to block its event loop or spin a nested event loop until the data has been read from disk. \u00a0Browsers avoid this now by trying to preload the Entire contents of local storage for your origin into memory as soon as they know your page is being loaded. \u00a0And then they keep that information, ALL of it, in memory until your page is gone.<\/p>\n<p>So if you store a megabyte of data in local storage, that\u2019s a megabyte of data that needs to be loaded in its entirety before you can use any of it, and that hangs around in scarce phone memory.<\/p>\n<p>To really make the point: do not use local storage, at least not directly. \u00a0Use a library like <a href=\"https:\/\/github.com\/mozilla\/localForage\">localForage<\/a> that will use <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/IndexedDB_API\">IndexedDB<\/a> when available, and then fails over to <a href=\"http:\/\/en.wikipedia.org\/wiki\/Web_SQL_Database\">WebSQLDatabase<\/a> and local storage in that order.<\/p>\n<p>Now, having sufficiently warned you of the terrible evils of local storage, I can say with a sorta-clear conscience\u2026 there are upsides in this very specific case.<\/p>\n<p>The synchronous nature of the API means that once we get our turn in the event loop we can act immediately. \u00a0There\u2019s no waiting around for an IndexedDB read result to gets its turn on the event loop.<\/p>\n<p>This matters because although the concept of loading is simple from a User Experience perspective, there\u2019s no standard to back it up right now. \u00a0Firefox OS\u2019s UX desires are very straightforward. \u00a0When you tap on an app, we zoom it in. \u00a0Until the app is loaded we display the app\u2019s icon in the center of the screen. \u00a0Unfortunately the standards are still assuming that the content is right there in the HTML. \u00a0This works well for document-based web pages or server-powered web apps where the contents of the page are baked in. \u00a0They work less well for client-only web apps where the content lives in a database and has to be dynamically retrieved.<\/p>\n<p>The two events that exist are:<\/p>\n<p>\u201c<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/Events\/DOMContentLoaded\">DOMContentLoaded<\/a>\u201d fires when the document has been fully parsed and all scripts not tagged as \u201casync\u201d have run. \u00a0If there were stylesheets referenced prior to the script tags, the script tags will wait for the stylesheet loads.<\/p>\n<p>\u201c<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/Events\/load\">load<\/a>\u201d fires when the document has been fully loaded; stylesheets, images, everything.<\/p>\n<p>But none of these have anything to do with the content in the page saying it\u2019s actually done. \u00a0This matters because these standards also say nothing about IndexedDB reads or the like. \u00a0We tried to create a standards consensus around this, but it\u2019s not there yet. \u00a0So Firefox OS just uses the \u201cload\u201d event to decide an app or page has finished loading and it can stop showing your app icon. \u00a0This largely avoids the dreaded \u201cflash of unstyled content\u201d problem, but it also means that your webpage or app needs to deal with this period of time by displaying a loading UI or just accepting a potentially awkward transient UI state.<\/p>\n<p><b>(Trivial HTML slide)<\/b><\/p>\n<div class=\"textLayer\">\n<blockquote>\n<pre data-canvas-width=\"560.5777333333332\">&lt;link rel=\u201dstylesheet\u201d ...&gt;\r\n&lt;script ...&gt;&lt;\/script&gt;\r\nDOMContentLoaded!<\/pre>\n<\/blockquote>\n<\/div>\n<p>This is the important summary of our index.html.<\/p>\n<p>We reference our stylesheet first. \u00a0It includes all of our styles. \u00a0We never dynamically load stylesheets because that compels a style recalculation for all nodes and potentially a reflow. \u00a0We would have to have an awful lot of style declarations before considering that.<\/p>\n<p>Then we have our single script file. \u00a0Because the stylesheet precedes the script, our script will not execute until the stylesheet has been loaded. \u00a0Then our script runs and we synchronously insert our HTML from local storage. \u00a0Then DOMContentLoaded can fire. \u00a0At this point the layout engine has enough information to perform a style recalculation and determine what CSS-referenced image resources need to be loaded for buttons and icons, then those load, and then we\u2019re good to be displayed as the \u201cload\u201d event can fire.<\/p>\n<p>After that, we\u2019re displaying an interactive-ish HTML document. \u00a0You can scroll, you can press on buttons and the :active state will apply. \u00a0So things seem real.<\/p>\n<p><b>Being Fast: Lazy Loading and Optimized Layers<\/b><\/p>\n<p>But now we need to try and get some logic in place as quickly as possible that will actually cash the checks that real-looking HTML UI is writing. \u00a0And the key to that is only loading what you need when you need it, and trying to get it to load as quickly as possible.<\/p>\n<p>There are many module loading and build optimizing tools out there, and most frameworks have a preferred or required way of handling this. \u00a0We used the RequireJS family of Asynchronous Module Definition loaders, specifically the alameda loader and the r-dot-js optimizer.<\/p>\n<p>One of the niceties of the loader plugin model is that we are able to express <i>resource<\/i> dependencies as well as <i>code<\/i> dependencies.<\/p>\n<p><b>RequireJS Loader Plugins<\/b><\/p>\n<blockquote>\n<pre data-canvas-width=\"514.12206888\">var fooModule = require('.\/foo');<\/pre>\n<pre data-canvas-width=\"659.4665953050002\">var htmlString = require('text!.\/foo.html');<\/pre>\n<pre data-canvas-width=\"812.2151214449999\">var localizedDomNode = require('tmpl!.\/foo.html');<\/pre>\n<\/blockquote>\n<p>The standard Common JS loader semantics used by node.js and io.js are the first one you see here. \u00a0Load the module, return its exports.<\/p>\n<p>But RequireJS loader plugins also allow us to do things like the second line where the exclamation point indicates that the load should occur using a loader plugin, which is itself a module that conforms to the loader plugin contract. \u00a0In this case it\u2019s saying load the file foo.html as raw text and return it as a string.<\/p>\n<p>But, wait, there\u2019s more! \u00a0loader plugins can do more than that. \u00a0The third example uses a loader that loads the HTML file using the \u2018text\u2019 plugin under the hood, creates an HTML document fragment, and pre-localizes it using our localization library. \u00a0And this works un-optimized in a browser, no compilation step needed, but it can also be optimized.<\/p>\n<p>So when our optimizer runs, it bundles up the core modules we use, plus, the modules for our \u201cmessage list\u201d card that displays the inbox. \u00a0And the message list card loads its HTML snippets using the template loader plugin. \u00a0The r-dot-js optimizer then locates these dependencies and the loader plugins also have optimizer logic that results in the HTML strings being inlined in the resulting optimized file. \u00a0So there\u2019s just one single javascript file to load with no extra HTML file dependencies or other loads.<\/p>\n<p>We then also run the optimizer against our other important cards like the \u201ccompose\u201d card and the \u201cmessage reader\u201d card. \u00a0We don\u2019t do this for all cards because it can be hard to carve up the module dependency graph for optimization without starting to run into cases of overlap where many optimized files redundantly include files loaded by other optimized files.<\/p>\n<p>Plus, we have another trick up our sleeve:<\/p>\n<p><b>Seeming Fast: Preloading<\/b><\/p>\n<p>Preloading. \u00a0Our cards optionally know the other cards they can load. \u00a0So once we display a card, we can kick off a preload of the cards that might potentially be displayed. \u00a0For example, the message list card can trigger the compose card and the message reader card, so we can trigger a preload of both of those.<\/p>\n<p>But we don\u2019t go overboard with preloading in the frontend because we still haven\u2019t actually loaded the back-end that actually does all the emaily email stuff. \u00a0The back-end is also chopped up into optimized layers along account type lines and online\/offline needs, but the main optimized JS file still weighs in at something like 17 thousand lines of code with newlines retained.<\/p>\n<p>So once our UI logic is loaded, it\u2019s time to kick-off loading the back-end. \u00a0And in order to avoid impacting the responsiveness of the UI both while it loads and when we\u2019re doing steady-state processing, we run it in a DOM Worker.<\/p>\n<p><b>Being Responsive: Workers and SharedWorkers<\/b><\/p>\n<p>DOM Workers are background JS threads that lack access to the page\u2019s DOM, communicating with their owning page via message passing with postMessage. \u00a0Normal workers are owned by a single page. \u00a0SharedWorkers can be accessed via multiple pages from the same document origin.<\/p>\n<p>By doing this, we stay out of the way of the main thread. \u00a0This is getting less important as browser engines support Asynchronous Panning &amp; Zooming or \u201cAPZ\u201d with hardware-accelerated composition, tile-based rendering, and all that good stuff. \u00a0(Some might even call it magic.)<\/p>\n<p>When Firefox OS started, we didn\u2019t have APZ, so any main-thread logic had the serious potential to result in janky scrolling and the impossibility of rendering at 60 frames per second. \u00a0It\u2019s a lot easier to get 60 frames-per-second now, but even asynchronous pan and zoom potentially has to wait on dispatching an event to the main thread to figure out if the user\u2019s tap is going to be consumed by app logic and preventDefault called on it. \u00a0APZ does this because it needs to know whether it should start scrolling or not.<\/p>\n<p>And speaking of 60 frames-per-second&#8230;<\/p>\n<p><b>Being Fast: Virtual List Widgets<\/b><\/p>\n<p>&#8230;the heart of a mail application is the message list. \u00a0The expected UX is to be able to fling your way through the entire list of what the email app knows about and see the messages there, just like you would on a native app.<\/p>\n<p>This is admittedly one of the areas where native apps have it easier. \u00a0There are usually list widgets that explicitly have a contract that says they request data on an as-needed basis. \u00a0They potentially even include data bindings so you can just point them at a data-store.<\/p>\n<p>But HTML doesn\u2019t yet have a concept of instantiate-on-demand for the DOM, although it\u2019s being discussed by Firefox layout engine developers. \u00a0For app purposes, the DOM is a scene graph. \u00a0An extremely capable scene graph that can handle huge documents, but there are footguns and it\u2019s arguably better to err on the side of fewer DOM nodes.<\/p>\n<p>So what the email app does is we create a scroll-region div and explicitly size it based on the number of messages in the mail folder we\u2019re displaying. \u00a0We create and render enough message summary nodes to cover the current screen, 3 screens worth of messages in the direction we\u2019re scrolling, and then we also retain up to 3 screens worth in the direction we scrolled from. \u00a0We also pre-fetch 2 more screens worth of messages from the database. \u00a0These constants were arrived at experimentally on prototype devices.<\/p>\n<p>We listen to \u201cscroll\u201d events and issue database requests and move DOM nodes around and update them as the user scrolls. \u00a0For any potentially jarring or expensive transitions such as coordinate space changes from new messages being added above the current scroll position, we wait for scrolling to stop.<\/p>\n<p>Nodes are absolutely positioned within the scroll area using their \u2018top\u2019 style but translation transforms also work. \u00a0We remove nodes from the DOM, then update their position and their state before re-appending them. \u00a0We do this because the browser APZ logic tries to be clever and figure out how to create an efficient series of layers so that it can pre-paint as much of the DOM as possible in graphic buffers, AKA layers, that can be efficiently composited by the GPU. \u00a0Its goal is that when the user is scrolling, or something is being animated, that it can just move the layers around the screen or adjust their opacity or other transforms without having to ask the layout engine to re-render portions of the DOM.<\/p>\n<p>When our message elements are added to the DOM with an already-initialized absolute position, the APZ logic lumps them together as something it can paint in a single layer along with the other elements in the scrolling region. \u00a0But if we start moving them around while they\u2019re still in the DOM, the layerization logic decides that they might want to independently move around more in the future and so each message item ends up in its own layer. \u00a0This slows things down. \u00a0But by removing them and re-adding them it sees them as new with static positions and decides that it can lump them all together in a single layer. \u00a0Really, we could just create new DOM nodes, but we produce slightly less garbage this way and in the event there\u2019s a bug, it\u2019s nicer to mess up with 30 DOM nodes displayed incorrectly rather than 3 million.<\/p>\n<p>But as neat as the layerization stuff is to know about on its own, I really mention it to underscore 2 suggestions:<\/p>\n<p>1, Use a library when possible. \u00a0Getting on and staying on APZ fast-paths is not trivial, especially across browser engines. \u00a0So it\u2019s a very good idea to use a library rather than rolling your own.<\/p>\n<p>2, Use developer tools. \u00a0APZ is tricky to reason about and even the developers who write the Async pan &amp; zoom logic can be surprised by what happens in complex real-world situations. \u00a0And there ARE developer tools available that help you avoid needing to reason about this. \u00a0Firefox OS has easy on-device developer tools that can help diagnose what\u2019s going on or at least help tell you whether you\u2019re making things faster or slower:<\/p>\n<p>&#8211; it\u2019s got a frames-per-second overlay; you do need to scroll like mad to get the system to want to render 60 frames-per-second, but it makes it clear what the net result is<\/p>\n<p>&#8211; it has paint flashing that overlays random colors every time it paints the DOM into a layer. \u00a0If the screen is flashing like a discotheque or has a lot of smeared rainbows, you know something\u2019s wrong because the APZ logic is not able to to just reuse its layers.<\/p>\n<p>&#8211; devtools can enable drawing cool colored borders around the layers APZ has created so you can see if layerization is doing something crazy<\/p>\n<p>There\u2019s also fancier and more complicated tools in Firefox and other browsers like Google Chrome to let you see what got painted, what the layer tree looks like, et cetera.<\/p>\n<p>And that\u2019s my spiel.<\/p>\n<p><b>Links<\/b><\/p>\n<p>The source code to Gaia can be found at <a href=\"https:\/\/github.com\/mozilla-b2g\/gaia\">https:\/\/github.com\/mozilla-b2g\/gaia<\/a><\/p>\n<p>The email app in particular can be found at <a href=\"https:\/\/github.com\/mozilla-b2g\/gaia\/tree\/master\/apps\/email\">https:\/\/github.com\/mozilla-b2g\/gaia\/tree\/master\/apps\/email<\/a><\/p>\n<p>(I also asked for questions here.)<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Last week I gave a talk at the Philly Tech Week 2015 Dev Day organized by the delightful people at technical.ly on some of the tricks\/strategies we use in the Firefox OS Gaia Email app.\u00a0 Note that the credit for &hellip; <a href=\"https:\/\/www.visophyte.org\/blog\/2015\/04\/30\/talk-script-firefox-os-email-performance-strategies\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[116,3],"tags":[125],"class_list":["post-900","post","type-post","status-publish","format-standard","hentry","category-firefox-os","category-mozilla","tag-talks"],"_links":{"self":[{"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/posts\/900","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/comments?post=900"}],"version-history":[{"count":2,"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/posts\/900\/revisions"}],"predecessor-version":[{"id":905,"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/posts\/900\/revisions\/905"}],"wp:attachment":[{"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/media?parent=900"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/categories?post=900"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.visophyte.org\/blog\/wp-json\/wp\/v2\/tags?post=900"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}