Xojo Web 2.0 FastStart

Please see https://forum.xojo.com/t/web-2-0-when-using-https-initial-page-load-is-500msec-slower/ for background details.

Xojo web 2.0 can be slow to load the first page because it’s making over 25 requests for small files (CSS and JavaScript mostly). Even if these files can be cached, the browser still much check each one, and this takes time.

The question came up “would it be faster to inline these into a single file” which is one of the recommendations here: https://developers.google.com/speed/docs/insights/BlockingJS

I’ve put together a proof of concept which I’m calling FastStart

Here’s the outline of what it does:

  • App.HandleURL : add code to handle a new special URL “fast.html”
  • Upon first connection to /fast.html, the app makes a URLconnection to “/” which gets the Xojo web default HTML. This file is about 60 lines, most of which are links to load script and CSS tags
  • process this HTML line by line
  • for each CSS or Script tag, extract the URL, make a new URLConnection to localhost and get the content of the file
  • Inline it: replace the script (or css) tag with the contents of the file
  • if it’s not a script or css line, then just add the line to the output
  • cache the data
  • Send the WebResponse with the final output

On subsequent connections after the first one, we can skip most of these steps and simply use the cached, inlined HTML – all we have to do is replace the SessionID with the new session ID.

Does it work?

Tests of Xojo Web FastStart

All tests are measuring the time until the first screen paint (e.g. when the web app UI is shown and is usable). All tests are using HTTPS.

Test 1:
Server is on LAN (gigabit ethernet), sub millisecond ping time.

Time Condition
1.8 normal
2.2 normal, cached (!note - this is slower than uncached!!)
1.4 fastStart, first session, uncached (includes initial parsing time)
0.5 fastStart, cached
0.5 fastStart, uncached

Conclusion: on a fast LAN, fastStart is much faster, even though the 1.73MB inlined HTML file can not be cached. Xojo Web is actually slower when the files are cached (possibly suggesting some issue with the 304 responses?)

Tests 2 and 3 are using using Network Link Conditioner to simulate a slow connection.
See https://download.developer.apple.com/Developer_Tools/Additional_Tools_for_Xcode_12.5/Additional_Tools_for_Xcode_12.5.dmg

Test 2:
Simulated LTE (50mbps/10mbps with 65msec delay)

Time Condition
4.5 normal
4.1 normal, cached
1.9 fastStart, first session, uncached (includes initial parsing time)
1.2 fastStart, uncached
0.9 fastStart, cached

Conclusion: on a good LTE connection, fastStart is much faster in all cases

Test 3:
Simulated 3G (780kbps/330kbps with 100msec delay)

Time Condition
6.7 normal
4.3 normal, cached
7.1 fastStart, first session, uncached (includes initial parsing time)
6.8 fastStart, uncached
6.7 fastStart, cached

Conclusion: on a 3G connection, the lack of caching starts to matter, and fastStart can be slower than normal.

1 Like

I made an alternate version of FastStart which allows caching. This version collects all the JavaScript into a single file, and all the CSS style sheets into a second file.

These files are then delivered with

Cache-Control: max-age=365000000, immutable

which allows caching.

Performance is pretty amazing.

Test 4: Version of fastStart
Simulated 3G (780kbps/330kbps with 100msec delay)

Time Condition
6.5 normal
3.9 normal, cached
6.5 fastStart, uncached
0.7 fastStart, cached

Conclusion: this method seems to have the best of both worlds. It’s no slower on first load, and gives excellent performance once cached. On a poor connection, the “time to usable UI” is over 5x faster than stock Xojo web 2.0 behavior.

Here’s the source code if anyone wants to test it out:

Put this inside WebApplication.HandleURL, and then load the app using the URL /fast-cache.html


// Xojo Web FastStart version 2
// For testing only

static baseHTML as string = ""
static baseSessionID as string = ""

dim version as string = XojoVersionString + "-" + str(app.NonReleaseVersion) // e.g. "2021r2.1-72"

static fastCacheJS as string = ""
static fastCacheCSS as string = ""



select case request.Path
  
  
case "fast-cache-" + version + ".js"
  // serve the cached combined JavaScript file
  response.MIMEType = "text/javascript"
  
  const etag1 = "1234567890"
  if Request.Header("If-none-match") = etag1 then
    response.Status = 304 // unchanged
  else
    response.Status = 200
    response.write fastCacheJS
  end if
  
  response.Header("Cache-Control") = "max-age=365000000, immutable" // never expires, no revalidation needed
  response.Header("ETag") = etag1
  
  
  return true
  
  
case "fast-cache-" + version + ".css"
  // serve the cached combined CSS file
  response.MIMEType = "text/css"
  
  const etag2 = "9876543210"
  if Request.Header("If-none-match") = etag2 then
    response.Status = 304 // unchanged
  else
    response.Status = 200
    response.write fastCacheCSS
  end if
  
  response.Header("Cache-Control") = "max-age=365000000, immutable" // never expires, no revalidation needed
  response.Header("ETag") = etag2
  return true
  
  
  
case "fast-cache.html"
  // Combine the JS and CSS files into one CSS and one JS cache file, which allows caching
  
  dim CR as string = EndOfLine.UNIX
  
  system.debugLog CurrentMethodName + " Fast Cache Start Request"
  dim host as string = "http://127.0.0.1:" + str(app.Port) + "/"
  
  // first, make a request to "/" and grab the new session token
  dim u as new URLConnection
  dim h as string = u.SendSync("GET", host)
  h = h.DefineEncoding(Encodings.UTF8)
  h = h.ReplaceLineEndings(CR)
  
  // get the session token which will be in this line
  '   XojoWeb.session.initialize('00F99B1E6B62715C2AD6BDFC00E66F65B38368084C36333D5015C4B3F4DA3A08')
  const tag = "XojoWeb.session.initialize('"
  dim x1 as integer = h.IndexOf(tag)
  dim sessionID as string = h.Middle(x1)
  sessionID = sessionID.NthField("'",2)
  
  system.debugLog CurrentMethodName + " Fast Cache Start Request - sessionID=" + sessionID
  
  // first run?  prepare the CSS and JS cache files
  dim h2 as string 
  if baseHTML = "" then
    system.debugLog CurrentMethodName + "  Fast Start : first run setup, preparing JS and CSS Caches " 
    baseSessionID = sessionID
    
    // parse each <script src=...> tag and stylesheet tag and extract 
    
    dim lines() as string = h.Split(CR)
    for each L as string in lines
      if L.IndexOf("<script type=") >= 0 then  // important: match on "<script type=" rather than "<script" to avoid matching the one "<script >" tag
        
        // extract the URL
        dim url as string = L.NthField("""",4)
        // remove leading "/"
        url = url.middle(1)
        
        
        if url = "" then break
        
        // get the script
        dim u2 as new URLConnection
        dim script as string = u2.SendSync("GET", host + url)
        script = script.DefineEncoding(Encodings.UTF8)
        script = script.ReplaceLineEndings(EndOfLine.UNIX)
        
        // inline it
        system.debugLog "*   Fast Cache Start: extracting " + url
        fastCacheJS = fastCacheJS + CR + CR + "<!-- FastStart: inlining script '" + url + "'" +  " -->" + CR +_ 
        script  + CR
        
        
      elseif L.IndexOf("<link type=""text/css""") >= 0 then  
        
        // extract the URL
        dim url as string = L.NthField("""",4)
        // remove leading "/"
        url = url.middle(1)
        
        
        if url = "" then break
        system.debugLog "*   Fast Cache Start: extracting " + url
        
        // get the script
        dim u2 as new URLConnection
        dim script as string = u2.SendSync("GET", host + url)
        script = script.DefineEncoding(Encodings.UTF8)
        script = script.ReplaceLineEndings(EndOfLine.UNIX)
        
        // inline it
        fastCacheCSS = fastCacheCSS + CR + CR + "<!-- FastStart: inlining CSS '" + url + "'" +  " -->" + CR +_ 
        "<style>" + CR + _
        script  + CR + _
        "</style>" + CR
        
        
      else
        h2 = h2 + L + EndOfLine.UNIX
      end if
      
      
    next
    
    // insert CSS cache in head
    
    h2 = h2.Replace("</head>", _
    "<link type=""text/css"" href=""/fast-cache-" + version + ".css"" rel=""stylesheet"">" + CR +_
    "</head>")
    
    
    // insert JS cache in body
    
    h2 = h2.Replace("<script >", _
    "<script type=""text/javascript"" src=""/fast-cache-" + version + ".js""></script>" + CR +_
    "<script >")
    
    
    // cache it
    
    baseHTML = h2
    
    
  end if
  
  
  
  
  // send the response after substituting the new sessionID
  h2 = baseHTML
  h2 = h2.Replace(baseSessionID, sessionID)
  response.MIMEType = "text/html"
  response.Status = 200
  response.write h2
  return true
  
  
    
  
else
  response.MIMEType = "text/text"
  response.Status = 404
  response.write "File not found"

  return true
  
end select




4 Likes

I’d love to try this out Mike, but am having issues. Will this run from the debugger? I’m failing after debug line first run setup, preparing JS and CSS caches. Tested on Safari on OS11 IDE.

Yes, it should work in the debugger. I’m using this in Xojo 2021 R2.1 - what version are you using?

When you say “failing” - what exactly is happening?

But it is not the perfect way it was designed. so it doesnt matter :rofl:

Jokes apart, I did some fixes to my web 1.0 apps by editing the files in the “Xojo Resources\WebFrameworks\Global” folder. I dont use web 2 but, can you chec if it is possible to change the files directly and avoid the need for HandleURL?

I do see the JS and CSS files there, but don’t see the master HTML file. (I didn’t look very hard). So your technique may or may not be possible.