📜 ⬆️ ⬇️

We speed up the service from the client side several times. Ajax + preloading in the background + when you hover

When databases and server are configured, queries are optimized, all caches are turned on, what options are left to speed up the service? The last level of abstraction, the user interface, allows you to achieve a speed increase from almost nothing. These three simple recipes will help not only to speed up the site several times, but also add some convenient buns to it. In the first part, we will easily transfer the site to primitive ajax-navigation. In the second, add preloading pages when you hover the mouse and just in the background. Overcome the speed of sound? We jump!




Ajax transitions between pages with instant return back.


Just a couple of days ago there was an article on this topic. It so happened that my article was already ready at that time, so the part about ajax will partly repeat a colleague. Take it as yet another experience and proof of how simple it is. In addition, my alternative will touch on several important aspects that were not considered by a colleague, and more deeply plunge into the practical part, allowing from scratch to fully implement navigation based on the code from the article.
')
Implementing ajax transitions in a simple form is not that difficult. Immediately make a reservation:
Surely there are ready-made libraries that make it even easier and more accurate. The purpose of this article is to show that it is easy to do with your hands. What for? For example, to adapt the script specifically for your site. The example in the article covers far from all aspects of ajax transitions (for example, work without history api will not be affected (normal navigation will be preserved for such browsers), or script version control). All aspects will not fit into the article, but will definitely be mentioned.

Explain briefly what a foreigner is talking about?
Have you seen VKontakte audio player, working without stopping when navigating between pages? This is because the pages are not reloaded, when you click on each link, javascript sends a request to the server, receives a page in response, and replaces the contents of the current page with it. Obvious advantages of this approach (except the ability to make a continuous player): deeper splitting a site into components allows you to win in the page loading speed (for example, you can not reload the static header of the site), and already loaded scripts do not need to be downloaded again, which saves requests, which also gives speed increase (this task is not always solved by the cache in the browser, for example, in the case of connected third-party dynamic libraries). In my case, switching to ajax is dictated by the inability to cache Google Charts, which js was given for more than a second at each transition and was not cached, and the license was locally prohibited to save it.

Server side preparation.


MVC, JSON, wrapping page content.
If you still have not separated the logic from the presentation, it's time to do it. To give a page through ajax, you need to get its contents in the form of a line. Therefore, if your code alternates with the output of intermediate data, you will need serious refactoring (it is still needed and will make life easier in the future): you need to ensure that all data for output is formed at the end, using a template (even regular functions returning the string can perform their role and independent of each other), to which variables with prepared dynamic data are transferred.

When clicking on links, the page will now be rendered in JavaScript, so it must be packaged in some format. For simplicity, use JSON.
We divide the data into components: html code, inline-scripts, page title, and information about redirect-e. Let's write a function that will receive these components as input, and return the result of the query to JS when clicking on the link (below is pseudocode, it was based on php, but you may have a different language):

function response(html, options = null) { define res; //     JSON res['html'] = html; // html  html- . //      html,   – error. if (options) { if (options['script']) { res['script'] = options['script']; } if (options['redirect']) { res['redirect'] = options['redirect']; } if (options['title']) { res['title'] = options['title']; } } //       json   UTF-8 header('Content-Type: application/json; charset=UTF-8'); print json_encode($res); //  res  json     http. exit(); //       json . } 


Now, in order to render the page during an ajax transition, it will be sufficient to call the response () function.
For example, for php, it will look like this:
 function wrapAbout($employees) { return "<div>   2012 .    : {$employees}</div>"; } $employees = getEmployeesNumber(); $html = wrapAbout($employees); response($html, array('title' => ' ')); 


Error processing.
It is not always possible to return the page. Error information will be processed separately.
To do this, we write the function:
 function error(num, msg) { define res; //,     JSON define res['error']; //     res['error']['num'] = $num; //  res['error']['msg'] = $msg; //          print json_encode(res); exit(); } 


Now, if an exception has occurred, or incorrect data has been transmitted, we respond as follows:
 if ($_POST['password'] != $password) { error(1, 'Invalid password'); } 


Support direct requests.
And what if the user did not click on the link, but went directly, or pressed F5? Such cases also need to be handled. For this, we optimized the code so that the line with html is returned. It is enough to give this html directly, and not in the form of json in cases where the request did not come from ajax. How to understand this? When querying from ajax, we will pass an additional parameter ajax = 1, so we can always discern where the query came from and how to return the result.

Client part


Client part preparation
Here we are also waiting for a small refactoring. There is such a thing as "page status". We no longer move between pages, everything happens on one, so the states need to be isolated. We want to work with the back button, instantly returning to the previous state of the page, and at the same time we want to have all the variables in the javascript environment. To do this, we will store the state in a single entity storing the full current state. It sounds scary, looks trivial:
 cur = {somevar: true}; 

We will store all the variables of the page inside cur - then we can easily save its state.
Let's get an auxiliary cache where the contents of the previous pages will be stored a couple of steps back:
 var globalPageHtml = []; 


Processing links

Links are no longer links, but buttons that click on to execute JavaScript, which loads another page via ajax. At the same time, it is necessary to preserve the possibility of a usual transition, in case ajax navigation is not available (for example, if history api is not supported and we cannot change the URL, but do not want to use hash navigation). And you need to support ctrl + click to open the link in a new tab. Redo all links in this way:
 <a onclick="go('/someurl', null, event); return false;" href="/someurl">Some link</a> 
What is go? This is the most important method, that he will be engaged in navigation, it will be about him below.

Work with URL and history

To work with navigation history, we will use the popular history.js library. This will allow us to support hash navigation at the same time in the future, but for now we are building primitive ajax navigation, which means browsers lagging behind progress can be left behind.
We initialize the work with History (it is assumed that the project uses jquery) to support the work with the back button:

 var History = window.History; History.enabled = !!(window.history && history.pushState); //Temp turn off html4 if (History.enabled) { $(window).bind('statechange', function() { var State = History.getState(); //replacingState -  ,    ajax-  .      URL   ,      -   back     .           ajax-     . if (!cur.replacingState) { cur = State.data; //,   ,   history api,      history.js if (globalPageHtml[State.url]) { //       ? $('#content').html(globalPageHtml[State.url].html); // ,       content,    html     . Html    . if (globalPageHtml[State.url].scripts) {//     ,    ,   , jquery       DOM. var fragment = document.createDocumentFragment(); globalPageHtml[State.url].scripts.each( function(nodeIndex, scriptNode) { fragment.appendChild(scriptNode); //   ,            -    DOM. }); document.getElementById('content').appendChild(fragment); //    DOM  ,     . } window.scrollTo(globalPageHtml[State.url].scroll[0], globalPageHtml[State.url].scroll[1]); //        ,    . delete globalPageHtml[State.url]; //  } else { go(State.url, null, null, true); //    "" ,      (,         URL  ).       ,      . } } cur.replacingState = false; }); } 


Basic javascript for ajax transitions

That got to the most important. The go function is called when the link is clicked:
 function go(url, query, event, back) { if (!History.enabled || (event && (event.ctrlKey || event.metaKey))) { //  ctrl ( command  )   history api var requestString = url; if (query) { requestString += query; } if (!History.enabled) { window.location = requestString; //   history api,      } else { window.open(requestString, '_blank'); //    ctrl,      /,   ajax } return; } var query = query || {}; var urlString = (url.length > 0) ? url : $(location).attr('href').split('?', 1); // URL   ( ),    if (jQuery.param(query).length > 0) { urlString += '?' + jQuery.param(query); //    (        ) } urlString = window.decodeURI(urlString); //  .          URL  history api.   , ,   rewrite,       N     /post/N,  - /post?item=N -        ajax . $.extend(query, {ajax: 1}); //       ajax  (   ,   url    ) $.ajax({ url: url, global: false, type: "GET", data: query, dataType: "json", success: function (res) { if (res.html) { //ajax     html,         ,      if (!back) { //  ,      ,      var currentUrl = $(location).attr('href'); globalPageHtml[currentUrl] = new Object(); //    ,    url globalPageHtml[currentUrl].html = $('#content').html(); globalPageHtml[currentUrl].scroll = getScrollXY(); //     globalPageHtml[currentUrl].scripts = $('script:not([src])'); //   .      .          cur.   ,  -       (,      ),     -  ,      (disclaimer:       ).   inline  ,                 (    ). //   state, history api,   cur.replacingState = true; History.replaceState(cur, $(document).attr('title') || null, window.decodeURI(currentUrl) || null); //       cur,  ,       back,   cur.replacingState = true; History.pushState(null, res.title || null, urlString); // URL     } window.scrollTo(0, 0); //      document.getElementById('content').innerHTML = res.html; //, ,    cur = {}; $('script:not([src])').each( //  (      innerHTML,   html()  jquery        ) function(nodeIndex, scriptNode) { try {eval(scriptNode.innerHTML);} catch(e) {/*Do nothing*/} }); } else { errorAlert(res.error || 'Unable to load page ' + url); //errorAlert - ,     ,    } }, error: function (res) { errorAlert(res.error || 'Unable to load page ' + url); } }); } 

Everything! Ajax navigation is ready.

Important notes about aspects not considered in this approach.

More serious ajax navigation will take into account the work with hashes for IE, but the limitations of the system implemented by us are not limited to this.

Loading different scripts for different pages
In the current form it is assumed that all JS-libraries will be loaded when you first enter any page. This is good when you have a small service, but in a large project I would like to download only the js that is relevant only for this section of the site. Therefore, a more serious ajax navigation should be able to load and unload third-party js-modules. It’s easy to implement: just transfer the url list of scripts you need to load from the server, use some ready-made bootloader, and on the next ajax transition throw out the downloaded code (or you can leave it, if you’re fighting only for speed, you don’t mind the memory) ).

Script Versions
In the js-code bug and you just rolled out a new version? Problem: during ajax-navigation, scripts may not be updated for days - a page opened in the browser, inside which scripts are not downloaded with each request, but have long been in memory, prevents the user from receiving the update. There is a solution: transfer the version numbers of js-modules from the server at each ajax transition. If the module is updated, download its new version, replacing the previous one from memory. The same applies to css.

Global ajax cache
In a more serious ajax navigation system, it is convenient to implement a global query cache in order to manually cache some queries and pages. This can be useful for preloading when you hover the cursor. Actually, we now proceed to the preloading.

Preload pages

After we have ajax-navigation, simple but super functional features are available to us to speed up the service. The effect of these simple techniques exceeds all expectations: the work of the site becomes instantaneous. And preloading will help us in this. It can be divided into two fundamentally different methods: preloading in the background, and when you hover the cursor. The first is available for a fixed small number of sections of the site associated with the current page. For example, if there are five tabs on the Settings page - we can download these tabs after loading the main page in the background so that they can be opened immediately upon click. This technique does not give a ride for the list of products (and in general any list of links) - you cannot load the page of each product from the search results if the list is more than 5 items. This will help preload when you hover the mouse.

Preload in background


Consider an example based on the revised code from VKontakte. Suppose we have several tabs (instead of tabs, there can be any sections and links that it is reasonable to open instantly on the open page). We are in one of them. Simply execute the preloadTabs (currentTab) method at the end of the page load; We will transfer the current tab to it, so that the north will return information only about the missing tabs, without loading the already opened one. The general meaning of the approach is: instead of immediately downloading information about neighboring tabs, which takes time, we will load only one tab, and load the rest in the background immediately after the open tab is ready. The effect is fantastic. Let us consider an example:
 function preloadTabs(current) { cur.preloading = 1; //   ajax    (     ajax      ,      ),    .  ,    callback   (  createElement   DOM-,         -    ,    ). function(data, tab1Html, tab2Html, tab3Html) { var page = $('#content'); if (cur.section != 'tab1') { page.append(createElement('div', {innerHTML: tab1Html})); } if (cur.section != 'tab2') { page.append(createElement('div', {innerHTML: tab2Html})); } if (cur.section != 'tab3') { page.append(createElement('div', {innerHTML: tab3Html})); } cur.preloading = 2; //0 -   , 1 -      , 2 -   } 


The function that will be in the handler for clicks on links and actually switch tabs:
 function switchTab(section) { if (cur.section == section) { //    return; } var doSwitch = function(section) { hideCurrentSection(); //   id     cur.section = section; // id  showCurrentSection(); //  ,    ,     append ( display:none )  DOM   //      history  ,    }; if (!cur.preloading || cur.preloading == 1) { //     $('#load_progress').show(); // ,     ,    if (cur.preloading != 1) { preloadTabs(cur.section); } var waitPreload = setInterval(function() { if (cur.preloaded == 2) { //  $('#load_progress').hide(); //    clearInterval(waitPreload); doSwitch(section); } }, 100); } else { doSwitch(section); } } 

The convenience of this code is that it will work even when the tabs are not yet loaded (someone clicked on the link very quickly). If the download has not started yet, it will start it, and the waiting indicator will show, and if the loading of the tabs has been started, it will wait for it to complete, and only then will show the tab. Very simple, but the service starts to fly.

Preload on mouse over


Almost every click has a significant (in milliseconds) delay between the click itself and the period when the cursor is over the link. This allows you to have time to download the link before clicking on it, turning the transition into an instant one. Here you need to be careful: what if the user moves the cursor from point A to B, and our link came up on the way (and at the same time a couple dozens of others - let's not bomb the browser and ajax service with requests if the cursor just went through the links). It turns out that before the click the cursor is delayed over the link, and you can calculate this minimum time range between clicks, but after hovering over the link, you can load the page, but the download will begin after a sufficient time to understand exactly what we hover over with the intention to click , and not just passing through. Additionally, we will cache strictly by one link; if we preload the new one, the old one is pushed out of memory.

By hovering the cursor, we will call a function (for example, for the list of friends we load user profiles):
 function preloadUser(user_id) { cur.pendingPreloadUid = user_id; cur.pendingPreload = setTimeout(doPreloadUser, 50); //50ms -         } 


On the mouseout event (the cursor is moved away from the link), we will call:
 function cancelPreloadUser() { clearTimeout(cur.pendingPreload); } 


And if you managed to wait for 50ms, then preload the link:
 function doPreloadUser() { var uid = cur.pendingPreloadUid; if (cur.cachedUser && (uid != cur.cachedUser.uid)) { // -   ,     ,        unloadUser(cur.cachedUser.uid); } else { cur.cachedUser = new Object(); } cur.cachedUser.uid = uid; //  ajax ,      .     ,     ,           ,          ,      ,        .       cachedUser,    . } 

Do not forget to unload the link, if a request came to preload a new one; in the unloadUser (uid) function, you need to clear the global cache.



Thank you, if you heroically read this far, or appreciate the work in preparing this article. All described methods are implemented depending on the size of the project from several hours to several days. I really hope that this material will help you to speed up your web services in practice many times. Write comments and share in the comments your experience accelerating sites.
image

Source: https://habr.com/ru/post/154957/


All Articles