Preloaded Resources

Overview

Every HTML page depends on a horde of resources, such as text files (html/js/css/etc.), images and fonts. Unsurprisingly, most of the time spent during every page initialization (and in some rare cases, its continued execution) goes to resource loading and preprocessing. Most resources require some type of preprocessing before they are ready to use - HTML/CSS/JavaScript require parsing, images require decoding, etc. Every resource needs to be read from the hard drive as well. Performing the aforementioned operations before your View starts initializing is referred to as resource preloading. Preloaded resources can give you a huge performance boost in page initialization. You can even achieve single frame initialization if you manage to preload all resources for a given page.

Most resource loading optimizations consist of finding a convenient time to load the resource in advance, for example during a “Loading” screen. There are two main ways to do that with Cohtml views:

  • You can prompt Cohtml to preload and cache some types of resources to avoid processing them each time. For example, using the PreloadAndCacheStylesheet API you can ensure that all of your CSS resources are already parsed and waiting when your page starts loading. Note that Font and CSS resources are cached system-wide, meaning that all Views which share the resource will benefit.
  • You can preload resources in the memory yourself, so you can skip fetching them from the hard drive when they are requested later on. For example, using a simple hash table you can cache the content of your JavaScript text files, ensuring that they are instantly available to Cohtml when requested.

Cohtml resource preloading APIs

You can find example implementations of all APIs listed below in the Gameface Instaload sample.

Preloading fonts

You can register fonts in advance via the cohtml::System::RegisterFont API. Calling this API will schedule a stream resource request for the font as a work job. When the work is executed your OnResourceStreamRequest callback will be invoked. Usually, you would respond to this request with an ISyncStreamReader implementation that will read the font from the disk. To preload the font and avoid disk reads at runtime you can read and buffer the whole file inside your ISyncStreamReader implementation and later respond to read requests from that buffer. Fonts can be heavy memory-wise and Cohtml does cache font information for already displayed characters, so if you can’t spare the extra memory overhead you can use this only during initialization then release the buffer and continue reading from disk.

This API does not guarantee that the font will be loaded by the time your page first initializes, as explained in Common pitfalls.

Preloading CSS files

You can preload CSS files via the cohtml::System::PreloadAndCacheStylesheet API. Passing a CSS file to this API will trigger a corresponding OnResourceRequest callback to load the CSS file, parse it, then cache it inside Cohtml. Once loaded, the stylesheet will stay cached in Cohtml’s memory, making all future initialization of the same page or other pages that use it faster.

You can use RemoveStylesheetCacheEntry or ClearStylesheetCache to clear one or all pre-loaded stylesheets respectively after you are done using them.

This API does not guarantee that the CSS will be loaded by the time your page first initializes, as explained in Common pitfalls.

Preloading HTML

You can preload HTML via the cohtml::System::PreloadAndCacheHTML API.

Passing a URL to this API will trigger a corresponding OnResourceRequest callback to load a valid HTML document, asynchronously parse it, and then cache it inside Cohtml. The parsed HTML will stay cached in Cohtml’s memory and will be used by all views if it’s requested.

The cache uses a URL as an identifier for preloaded HTML documents and ignores query parameters. When preloading a URL with query parameters, the HTML document from the response will be cached under a URL without the query parameters. When a URL with query parameters is requested from the cache, the cache will serve the cached HTML document that matches the URL without the query parameters.

RemoveHTMLCacheEntry and ClearHTMLCache can be used to clear cache entries from the HTML cache. Note that removing an HTML from a cache that is currently in use by a view won’t free the occupied memory immediately. It will be freed after all Views finish DOM building, even if the HTML is not loaded yet.

Most commonly preloaded HTML content will be used for accelerated loading of pages in a View using the View::LoadURL API. The HTML cache is also accessible via XHRs in JS and is effective for loading partial HTML content on the page. This can be achieved by sending an XHR with the preloaded URL and using its unmodified response for inserting content with DOM APIs such as innerHTML or insertAdjacentHTML. Modifying the response will invalidate the cache and will trigger HTML parsing.

This API does not guarantee that the HTML will be loaded by the time of your first usage, as explained in Common pitfalls.

Preloading JS

You can preload JavaScript by compiling it ahead of time and providing the result of the compilation when responding to the resource request. This is done using the cohtml::ScriptCompiler::Compile API, which will return a cohtml::DataBuffer pointer, containing the optimized data. That data is used when responding to the resource request.

You should still fill in the file source in the response as it is usually done for every request, but in addition to that you can also set the optimized data for this resource.

You can check this code snippet, explaining what you should do to handle the JS file request and respond with the file source and optimized data:

// Lets create a fileReader, which is a simple file read object, that can read a file with UTF-8 encoding and write it inside a source buffer.
auto fileReader = CreateFileReader(filePath);

// We need to allocate space inside the response, as we usually do for all request-responses and then fill in the file source in that space.
auto fileSrcLength = fileReader.GetSize();
auto fileSrcBuff = response->GetSpace(fileSrcLength);
fileReader.Read(0, static_cast<unsigned char*>(fileSrcBuff), fileSrcLength);

// Creating a script compiler can be slow, and therefore it is recommended to cache and reuse the script compiler for multiple compilations,
// but in this snippet we will destroy it once we are done with the current compilation for the sake of simplicity.
if (cohtml::ScriptCompiler* compiler = m_Library->CreateScriptCompiler())
{
    // To create the sourceData buffer, we use the UTF-8 encoded file source and length.
    auto sourceData = cohtml::DataBuffer::CreateDataBuffer(
        fileSrcBuff, fileSrcSize,
        [](unsigned char* data, void* userData)
        {
            // This is a destruction callback, it can be used
            // for handling custom deleter logic when needed.
            (void)data;
            (void)userData;
        }, nullptr);

    // The same compiler can be used for multiple compilations.
    auto optimizedData = compiler->Compile(sourceData);

    // The sourceData should be released as it is no longer needed.
    sourceData->Release();

    if (optimizedData)
    {
        response->ReceiveOptimizedData(optimizedData);
        // The optimizedData can be released now or be cached and reused for future requests
        // for the same file from any View, and later released when it is no longer needed.
        optimizedData->Release();
    }

    compiler->Destroy();
}

// Finaly, we can finish the response.
response->Finish(cohtml::IAsyncResourceResponse::Success);

The optimized data can easily be serialized and deserialized, and that can be used to implement disk cache for JavaScript files. Responding to requests for JavaScript resources with optimized data makes the first compile and run of said JavaScript roughly three times faster.

For more information you can check the implementation inside the Instaload sample.

Preloading Images

Preloading image resources is a fairly complicated process, which rightfully deserves its own page.

Common pitfalls

Due to Cohtml’s concurrent resource parsing, there is no way to guarantee that your resource will actually be parsed by the time the page starts loading. This is true for HTML, CSS, and Font preloading APIs. Consider the following snippet, which uses CSS as an example:

System->PreloadAndCacheStylesheet("SomeFolder/common.css"); // stylesheet will start pre-loading here
View->LoadURL("my_url.html"); // let's assume this page uses common.css.
// ...
// later on, during your engine's frame execution
View->Advance(time);

In the example above, when your code reaches the View->Advance(time) line, Cohtml might still be loading common.css on another thread. In those types of cases, Cohtml will build your page without the resource, then update it on a later Advance call, after the resource is ready. In a similar way, a View won’t start building the DOM until the HTML is fully parsed.

If Fonts, CSS, JS and HTML resources are not preloaded, they will be requested on page load and can slow down the FinishLoad event. That is why you should preload them first and as early as possible. One good practice, for example, would be preloading such files before doing any image or raw file preloading.

Preloading resources in the memory

When no API is provided for a given resource type, you can still preload the resource contents in the memory. A straightforward implementation of this can work as follows:

  • At some point in time before the HTML page is loaded, read the contents of the resource files from the disk and store them in memory. You can find an example implementation of this in the ResourceHandler::AddPreloadedResource(path) function of the instaload sample.

      // Store file contents in memory
      for (auto& it : std::filesystem::recursive_directory_iterator(m_ResourcesRoot))
      {
          auto extention = it.path().extension();
          // JSON files are currently not preloadable by Cohtml, so they can be
          // preloaded in memory only.
          if (extention == ".json")
          {
              auto path = it.path().generic_string();
              // Assume ReadFile is a function that fetches the contents of a file in memory
              auto contents = ReadFile(path);
              m_PreloadedResources.Emplace(path, contents);
          }
      }
    
  • Modify your cohtml::IAsyncResourceHandler::OnResourceRequest implementation to check if the contents of the requested file are already loaded in memory. If they are, return them to Cohtml immediately, then signal that the request is finished.

      void OnResourceRequest(const cohtml::IAsyncResourceRequest* request,
                             cohtml::IAsyncResourceResponse* response)
    
          std::string path = GetPathFromRequest(request->GetURL());
          // Check if resource is preloaded
          auto findIt = m_PreloadedResources.find(request->path);
          if (findIt != m_PreloadedResources.end())
          {
              // Assume the function below passes the contents of the file
              // to Cohtml via the IAsyncResourceResponse API.
              PassFileContentsToCohtml(findIt-second, response);
              response->Finish(cohtml::IAsyncResourceResponse::Success);
          }
          else
          {
              // You can read the file from disk here and respond immediately,
              // or start some process that will eventually provide the file later on
          }