📜 ⬆️ ⬇️

Seven principles for creating modern web applications

This article is based on my presentation from the BrazilJS conference in August 2014. It is based on ideas that I recently wrote about in a blog , mainly in connection with UX and performance.

I want to present 7 effective principles for websites that want to use JavaScript to manage the UI. These principles are the result of my work as a web designer, but also as a long-time WWW user.

JavaScript has undoubtedly become an indispensable tool for frontend developers. Now its scope is expanding to other areas, such as servers and microcontrollers. This programming language was chosen by prestigious universities to teach students the basics of computer science.
')
At the same time, there are a number of questions regarding its role and specific use, which many find it difficult to answer, including the authors of frameworks and libraries.


Next will be my attempts to answer these questions. I tried to explore how to use JavaScript from a user's perspective (UX). In particular, he paid special attention to the idea of ​​minimizing the time it takes the user to retrieve data of interest. Starting from the basics of network technologies and ending with the prediction of the future behavior of the user.

1. Rendering of pages on the server


tl; DR : Rendering on the server is not done for SEO, but for performance. Take into account additional requests for getting scripts, styles and subsequent requests to the API. In the future, consider using the HTTP 2.0 Push method.

First of all, I have to pay attention to the common error of separating “server-rendered applications” and “one-page applications”. If we want to achieve the best perception from the point of view of the user, then we should not limit ourselves to such a framework and abandon one alternative in favor of another.

The reasons are quite obvious. Pages are transmitted over the Internet, which has physical limitations, which was unforgettably illustrated by Stuart Cheshire in the famous essay "This is latency, idiot" :

The distance between Stanford and Boston is 4320 km.
The speed of light in vacuum is 300 x 10 ^ 6 m / s.
The speed of light in fiber is about 66% of the speed of light in a vacuum.
The speed of light in the fiber is 300 x 10 ^ 6 m / s * 0.66 = 200 x 10 ^ 6 m / s.
One-way delay in transmission to Boston is 4320 km / 200 x 10 ^ 6 m / s = 21.6 ms.
The delay in the transfer back and forth 43.2 ms.
Ping from Stanford to Boston on the Internet of the modern sample of about 85 ms (...)
So, modern Internet equipment transmits a signal at a speed of 0.5 from the speed of light.

This result can be improved by 85 ms (and now it is slightly better), but it is important to understand that there is a physical limitation on the delay in transmitting information via the Internet, no matter how much the bandwidth on users' computers increases.

This is especially important due to the growing popularity of JavaScript applications, which usually contain only the <script> and <link> markup next to the empty <body> field. The so-called single-page applications (Single Page Applications, SPA) - the server returns one page, and everything else is called by the code on the client side.

Imagine a script when a user comes directly to pp.com/orders pp.com/orders . By the time your application receives and processes this request, it already has important information about what to show on the page. It can, for example, load an order from the database and add it to the response. But most of the SPA in this situation returns a blank page and the <script> tag. Then you have to exchange requests again to get the contents of the script, and again to get the content.


Analysis of the HTML sent by the server for each SPA page

Many developers deliberately make such a sacrifice. They try to ensure that additional network hops for a user occur only once, sending the correct headers for caching in the responses with scripts and CSS. The generally accepted opinion is that this is an acceptable transaction, because after downloading all the files to a computer, most of the user's actions (like switching to other sections) are performed without requesting additional pages or scripts.

However, even taking into account the cache, there is a certain performance loss, considering the time spent on parsing and script execution. In the article “Is jQuery too big for a mobile phone?” It says how jQuery alone can slow down some mobile browsers for hundreds of milliseconds.

Worse yet, usually the user does not receive any feedback while the scripts are being loaded. The result is a blank page on the screen, which then suddenly turns into a fully loaded page.

Most importantly, we usually forget that the most common transport for transmitting Internet data (TCP) starts slowly. This almost certainly ensures that most scripted suites will not be transferred at once, making the situation described above even worse.

A TCP connection begins with a packet exchange for a handshake. If you use SSL, which is important for the secure transfer of scripts, there are two additional packet exchanges (one if the client restores the session). Only after that the server can start sending data, but practice shows that it does this slowly and in portions.

A congestion control mechanism called Slow Start is built into the TCP protocol to send data, gradually increasing the number of segments . This has two serious conclusions for the SPA:

1. Large scripts load much longer than it seems. As explained by Ilya Grigorik’s book "High Performance Browser Networking," it takes "four packet exchanges (...) and hundreds of milliseconds of delay to reach 64 kilobytes of data exchange between the client and the server." For example, in the case of a fast Internet connection between London and New York, it takes 225 ms before TCP can reach the maximum packet size.

2. Since this rule also applies to the initial loading of the page, it is very important what content is loaded for rendering on the page in the first place. As Paul Irish concludes in his presentation “Delivery of Goods” , the first 14 KB are critical. This is understandable if you look at the chart with the indication of the volume of transmission between the client and the server in the first stages of establishing a connection.


How many KB server can send at each stage of the connection, by segments

Websites that manage to deliver content (even if the basic markup without data) in this window seem to be extremely responsive. In fact, many authors of fast server applications perceive JavaScript as something unnecessary or that needs to be used with great care. This attitude is further enhanced if the application has a fast backend and database, and its servers are located near users (CDN).

The role of the server in speeding up the presentation of content directly depends on the web application. The solution does not always come down to "rendering whole pages on the server."

In some cases, it is better to exclude part of the page that is currently irrelevant for the user from the initial answer and leave for later. Some applications, for example, prefer to render only the “core” of the page to ensure immediate response. Then they request different parts of the page in parallel. This provides better responsiveness even in a situation with a slow outdated backend. For some pages, rendering a visible part of the page will be a good option.

The qualitative assessment of scripts and styles is extremely important, taking into account the information that the server has about the session, the client and the URL. Scripts that sort the orders will obviously be more important for /orders than the logic of the settings page. Maybe not so obvious, but there is a difference in loading “structural CSS” and “CSS for styling.” The first may be needed for JavaScript code, so blocking is required, and the second is loaded asynchronously.

A good example of a SPA that does not lead to excessive packet exchange is a conceptual clone of StackOverflow in 4096 bytes , it can theoretically be loaded with the very first packet after a handshake on a TCP connection! The author managed to achieve this by refusing to cache, using inline for all resources in the response from the server. Using SPDY or HTTP / 2 server push , it is theoretically possible to transfer all cached client code in one hop. Well, currently, rendering parts or the entire page on the server side remains the most popular way to get rid of unnecessary rounds of packet exchange.


Proof-of-concept SPA using inline for CSS and JS to get rid of extra roundtrip

A fairly flexible system that divides the rendering between the browser and the server and provides tools for the gradual loading of scripts and styles can easily erase the distinction between web sites and web applications . Both use URLs, navigation, and demonstrate data to the user. Even a spreadsheet application that traditionally relies on client-side functionality must first show the client the information to be edited. And to do this in the least amount of roundtripes is of paramount importance.

From my point of view, the biggest lack of performance in many popular systems in our time is due to the progressive accumulation of complexity in the stack. Over time, technologies like JavaScript and CSS were added. Their popularity also gradually grew. Only now can we evaluate how they can be used differently. We are talking about improving the protocols (this shows the current progress of SPDY and QUIC), but the greatest benefit comes from the optimization of applications.

It will be useful to recall some historical discussions around the design of earlier versions of HTML and WWW. For example, this 1997 mailing list suggests adding an <img> tag in HTML. Marc Andressen repeats how important it is to quickly deliver information:

“If the document needs to be integrated into a single unit on the fly, then it can be arbitrarily difficult, and even if the complexity is limited, we still have major performance problems due to the structuring of documents in a similar way. First of all, this immediately violates the principle of one hop on the WWW (well, IMG also violates it, but for a very specific reason and in a very limited sense) - are we sure that we want it? ”


2. Immediate response to user actions


tl; DR : JavaScript allows you to hide network latency altogether. Using this as a design principle, we can even remove almost all loading indicators and “loading” messages from the application. PJAX or TurboLinks miss out on opportunities to increase the subjective speed of the interface.

Our task is to accelerate the maximum response to user actions. No matter how much effort we put into reducing the number of hops when working with a web application, but there are things beyond our control. This is the theoretical limit of the speed of light and the minimum ping between the client and the server.

An important factor is the unpredictable quality of communication between the client and the server. If the call quality is poor, then packet retransmission will occur. Where content needs to load in a couple of roundtrips, you may need a lot more.

This is the main advantage of JavaScript to improve UX. If on the client side the interface is controlled using scripts, we can hide the network latency. We can create the impression of high speed. We can artificially achieve a zero delay.

Suppose again that we have plain HTML. Documents are linked via hyperlinks or <a> tags. If you click on any of them, the browser will make a network request, which takes an unpredictable long time, then it receives and processes the received data and finally goes into a new state.

JavaScript allows you to respond immediately and optimistically to user actions. Clicking on a link or button leads to an immediate response, without contacting the Network. A well-known example is the Gmail (or Google Inbox) interface, in which email archiving occurs immediately, while the corresponding request to the server is sent and processed asynchronously.

In the case of a form, instead of waiting for some HTML code as an answer to filling it out, we can respond immediately as soon as the user clicks “Enter”. Or even better, as Google does, we can react even earlier by preparing markup for a new page in advance.



This behavior is an example of what I call markup adaptation . The basic idea is that the page “knows” its future markup, so it can switch to it when there is no data to indicate this. This is “optimistic” behavior, because there is still a risk that the data will never arrive, and you will have to display an error message, but this obviously happens rarely.

The main page of Google is quite suitable as an example, because it very clearly demonstrates the first two principles from our article.

First, a batch dump of TCP connections from www.google.com www.google.com shows that they specifically try to send the entire page immediately after receiving the request. The entire packet exchange, including closing the connection, takes 64 ms for me in San Francisco. This was probably true for them from the very beginning .

At the end of 2004, Google pioneered the use of JavaScript to provide real-time prompts during a search query (interestingly, an employee developed this function 20% of the time free from basic work, as well as Gmail). It even became the foundation for the emergence of Ajax :

Look at Google Suggest. Watch how search terms are updated as you type, almost instantly ... without any delay in reloading the page. Google Suggest and Google Maps are two examples of new approaches to creating web applications, which we called “Ajax” in Adaptive Path

And in 2010, they introduced Instant Search, in which JS plays a central role, generally excluding the page refresh manually and switching to the “search results” markup at the first keystroke, as seen in the illustration above.

Another prominent example of adapting markup may be in your pocket. From the very first days, iPhone OS demanded that application authors provide a default.png image, which can be immediately displayed on the screen while the application itself is loading.


iPhone OS forcibly loads default.png before launching the application

In this case, the operating system does not compensate for the network delay, but the CPU. This was important given the performance of the early equipment. True, in some cases, this approach failed. For example, if the picture did not match the password entry screen. A detailed analysis of the results published by Marco Arment in 2010 .

Another type of action, besides clicks and submitting forms, that are greatly improved with the help of JavaScript, is the rendering of the file download .

We can register a user’s attempt to load a file in various ways: drag-n-drop, pasting from the buffer, file selection. Then, thanks to the new HTML5 APIs , we can display the content as if it was already loaded. An example of this kind of interface is our work with downloads in Cloudup. Notice how the thumbnail image is generated and rendered instantly:


The image is rendered and displayed until the download is complete.

In all these cases, we improve the perception of speed . Fortunately, there is much evidence of the usefulness of this approach. Take at least an example of how increasing the distance to the baggage conveyor at the Houston airport reduced the number of complaints about lost baggage without having to speed up baggage handling.

This idea should seriously affect the UI of our applications. I believe that download indicators should be rare, especially if we are switching to applications with real-time information, which are described in the next section.

There are situations where the illusion of instantaneous action in reality has a detrimental effect on the UX. This may be a form of payment or end of the session on the site. Using an optimistic approach here, de facto deceiving the user, we risk irritating him.

But even in these cases, the display on the screen of spinners or loading indicators should be stopped. They need to be displayed only after the user regards the response as not instantaneous. According to the frequently quoted Nielsen study :

The basic advice on response time remains unchanged for thirty years Miller 1968; Card et al. 1991 :
* 0.1 seconds is the limit, so that the user perceives the response as immediate, there is no need to display any additional information, except for the result of the operation.
* 1.0 seconds is the limit on the continuity of the thought flow of the user, even though he will notice a delay. Usually, no additional indication is required when the delay is more than 0.1 second, but less than 1.0 second , but the user loses the feeling of direct work with the data.
* 10 seconds is a limit to keeping the user's attention on the dialogue. With a greater delay, users will want to perform another task, waiting for a response from the computer.

Techniques like PJAX or TurboLinks unfortunately miss most of the features described in this section. The code on the client side does not "know" about the future state of the page until it communicates with the server.

3. Reaction to data change


tl; DR : When data is updated on the server, the client should be notified without delay. This is such a form of increasing productivity when the user is freed from the need to perform additional actions (press F5, refresh the page). New problems: management (repeated) connection, restoration of a state.

The third principle relates to the response of the UI to changing data at the source, usually in one or more database servers.

The model of transferring HTML data that goes static until the user refreshes the page (traditional websites) or interacts with it (Ajax) goes into the past.

Your UI should be updated automatically .

This is critically important in a world with an increasing flow of information from various sources, including watches, phones, tablets and wearable devices that will appear in the future.

Imagine Facebook news feed immediately after its appearance, when information was published, mainly from users' personal computers. Static rendering could not be called optimal, but it made sense for people who updated the tape, say, once a day.

Now we live in the world when you upload a photo - and almost immediately get likes and comments from friends and acquaintances. The need for instant response has become a natural necessity in the competitive environment of other applications.

It would be wrong, however, to assume that the benefits of an instant UI update are limited only to multi-user applications. That's why I like to talk about agreed data points , instead of users . Take the typical photo synchronization script between your phone and your own laptop:


A single-user application can also benefit from "reactivity"

It is useful to present all the information that is sent to the user as "reactive". Synchronizing session and authorization status is one example of a universal approach. If the users of your application have several tabs open at the same time, then the end of a working session on one of them should immediately deactivate authorization on all the others. This inevitably leads to improved security and better protection of confidential information, especially in situations where several people have access to the same device.


Each page responds to session state and authorization status.

Once you have established the rule that the information on the screen is updated automatically, it is important to work on a new task: restoring the state.

When sending requests and receiving atomic updates, it is easy to forget that your application should be updated normally even after a long lack of communication. Imagine that you close the lid of the laptop and open it in a few days. How will the application behave?


An example of what happens in case of incorrect communication update.

The ability of the application to restore communication normally interacts with principle No. 1. If you choose to send data when the page is first loaded, you must also take into account the time that has passed before the scripts are loaded. This time is essentially equivalent to disconnect time, so the initial connection of your scripts is the resumption of the session.

4. Control data exchange with the server


tl; DR : Now we can fine-tune the communication with the server. Ensure error handling, repeated requests to the client, synchronization of data in the background, and keeping the cache offline.

When the web appeared, the data exchange between the client and the server was limited in several ways:

  1. Clicking on the link will send GETfor a new page and its rendering.
  2. POST GET .
  3. GET .

The simplicity of such a model is very attractive, and now everything is definitely complicated when it comes to understanding how to receive and send information.

The main restrictions relate to the second paragraph. The inability to send data without necessarily loading a new page was a disadvantage in terms of performance. But the most important thing is that it completely broke the "Back" button:


Probably the most annoying artifact of the old web.

That is why the web as a platform for applications remained incomplete without JavaScript. Ajax was a huge leap forward in terms of convenience in terms of publishing information by the user.

Now we have a lot of APIs (XMLHttpRequest, WebSocket, EventSource, these are just a few of them), which give full and clear control over the data flow. In addition to the ability to publish user data through the form, we have new opportunities for improving UX.

Directly related to the previous principle is showing the status of the connection . If we expect that the data will be updated automatically, we are obliged to inform the user about the facts of communication loss and attempts to restore it .

When a disconnect is detected, it is useful to store the data in memory (or even better, in localStorage), so that you can send it later. This is especially important in light of the future use of ServiceWorker.that allows javascript applications to run in the background . If your application is not open, you can still continue to try to synchronize data with the server in the background.

Consider the possibility of timeouts and errors when sending data, such situations should be decided in favor of the client . If the connection is reestablished, try sending the data again. In case of a permanent error, report this to the user.

Some errors need to be handled especially carefully. For example, an unexpected 403 can mean that a user session is invalidated. In such cases, it is possible to restore the session if you show the user a window for entering the login and password.

It is important to make sure that the user does not accidentally interrupt the flow of data. This can happen in two situations. The first and most obvious case is closing the browser or tab that we are trying to prevent with the handler beforeunload.


Warningbeforeunload

Another (and less obvious) case is an attempt to switch to another page, for example, clicking on a link. In this case, the application can stop the user by other methods, at the discretion of the developer.

5. Don't break history, improve it.


tl; DR : If the browser does not manage URLs and history, we will have new problems. Make sure you meet the expected scroll behavior. Save your own cache for quick feedback.

Apart from submitting forms, when using hyperlinks in the web application alone, we will have a fully functional “Forward / Back” navigation in the browser.

For example, a typical “endless” page is usually made using a javascript button that requests additional data / HTML and inserts it. Unfortunately, few people remember the need to call history.pushStateor replaceStateas a mandatory step.

That's why I use the word “break”. With a simple initial web model, this was not possible. Each state change was based on a URL change.

But there is also the other side of the coin - the ability to improve the history of surfing, which we now control using JavaScript.

One such opportunity, Daniel Pipius, called Fast Back :

The back button should work quickly; users do not expect too much data change.

This is how to consider the “Back” button as a button from a web application and apply principle 2 to it: immediately respond to a user action . The main thing is that you have the opportunity to decide how to organize the caching of the previous page and instantly display it on the screen. You can then apply the principle number 3, and then inform the user about new data on this page.

There are still a few situations where you can not control the behavior of the cache. For example, if you rendered a page, then went to a third-party site, and then the user clicked "Back." Applications that render HTML on the server side are especially susceptible to this small bug, and then modify it on the client side:


Incorrect operation of the Back button

Another way to break the navigation is to ignore the memory of the scrolling state. Once again, pages that do not use JS and manual history management will most likely not have problems here. But there will be dynamic pages. I tested two of the most popular JavaScript-based news feeds on the Internet: Twitter and Facebook. Both had scrolling amnesia.


Endless paging of pages is usually a sign of scrolling amnesia.

At the end of the day, beware of state changes that are only relevant when viewing history. For example, this is a case of changing the state of subtrees with comments.


Changing the type of comments you want to save in history

If the page has been re-drawn after clicking the link inside the application, the user can expect that all comments will be expanded. When you change the state, you need to save it in history.

6. Updating code via push messages


tl; DR : It is not enough to send only data through push messages, you also need the code. Avoid API errors and improve performance. Use a stateless DOM for painless application redistribution.

It is extremely important that your application reacts to changes in the code.

First, it reduces the number of possible errors and increases reliability. If you have made an important change in the backend API, then you need to update the client program code. Otherwise, customers may not accept new data or may send data in an incompatible format.

No less important is the observance of principle number 3. If your interface is updated itself, then users have little reason to turn to manual page reloading.

Keep in mind that for a regular site, updating a page triggers two things: reloading data and reloading code. Organizing a system with push updates of data without push updates of the code is defective, especially in a world where one tab (session) can remain open for a very long time.

If the server push channel is working, then the user can be sent a notification about the availability of new code. If not, the version number can be added to the outgoing HTTP requests header. The server can compare it with the latest known version, agree to process the request or not, and issue a task for the client.

After that, some web applications force the page to reload on behalf of the user. For example, if the page is not in the visible area of ​​the screenand there are no completed forms for input.

An even better approach is hot-code replacement . This means that you do not have to perform a full page reload. Instead, certain modules are replaced on the fly, and their code is resubmitted for execution.

In many existing applications, it is quite difficult to perform hot-swappable code. To do this, you must initially adhere to the architecture that separates the behavior (code) from the data (state). This separation will allow us to quickly roll a lot of different patches.

For example, in our web application, there is a module that installs a bus for passing events (like socket.io). When an event occurs, the state of a particular component changes and this is reflected in the DOM. Then you change the behavior of this component, for example, so that it generates different DOM markup for the existing and new states.

Ideally, we should be able to change the code modularly. You will not need to reconnect to the socket, for example, if you can simply update the code of the desired component. The ideal architecture for push code updates is thus modular.

But immediately there is a problem with how to evaluate modules without unwanted side effects. Here, an architecture like the one that React offers is best suited.. If the component code is updated, its logic can simply be re-executed, and the DOM is updated. An explanation of this concept from Dan Abramov is read here .

Essentially, the idea is that you update the DOM (or repaint it), which helps a lot in replacing the code. If the state is stored in the DOM or event handlers are set by the application, updating the code can be a much more difficult task.

7. Predicting behavior


tl; DR : Negative Delay.

Modern JavaScript applications can have mechanisms for predicting user actions.

The most obvious application of this idea is to download data in advance from the server before the user requests it. Downloading a webpage when the mouse cursor appears over it, so when you click on links, it is displayed instantly, this is a simple example.

A slightly more advanced method of monitoring mouse tracking analyzes its trajectory for future “collisions” with interactive elements, like buttons. JQuery example :


The jQuery plugin provides mouse trajectory

Conclusion


The web remains the most versatile medium of information transfer. We continue to add dynamics to our pages and, before their introduction, we must make sure that we keep the important web principles inherited by us.

Hyperlinked pages are good building blocks for any application. Progressive loading of code, styles, and markup as user actions ensure excellent performance without abandoning interactivity.

New unique features provides JavaScript. If these technologies are widely used, they will provide the best experience for users of the freest platform of all existing - WWW.

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


All Articles