HTML5 INSIGHT


*Мысль этого топика тривиальная и должна быть известна каждому: используйте асинхронные запросы. Но статистика – штука суровая, и, видимо, это знают не все.*

8.4% всех зависаний IE9 за прошедший месяц являются следствием того, что XMLHttpRequest объекты блокируют поток UI синхронным запросом. Это огромное число! С помощью нескольких доступных изменений в коде эти зависания можно легко избежать – и разработчики могут обеспечить своим пользователям намного лучшие впечатления от работы с их сайтами. Мы рассмотрим, что происходит при зависании, что вы можете с этим сделать, и мы также попробуем сделать небольшую демонстрацию, чтобы воочую посмотреть, что происходит, когда синхронный запрос подвешивает браузер.

Синхронный XMLHttpRequest блокирует поток UI

Мы уже знаем, что запуск блокирующих операций в потоке UI может приводить к проблемам. Мы уже писали об этом раньше. Это может быть неочевидным, но это именно то, что происходит, когда метод XMLHttpRequest.send() вызывается синхронно (например, путем передачи значения false для атрибута bAsync при вызове метода open()). Поток UI должен дождаться, пока получить ответ от сервера или пока не произойдет таймаут для запроса. Пока идет ожидание, сообщения не проходят.

Зачем вообще использовать синхронне XMLHttpRequest?

Разработчики стремятся к тому, чтобы использовать синхронные запросы вместо асинхронных просто потому, что это легче и может приводить к менее сложному коду. В этом обсуждении на StackOverflow приводятся некоторые весомые доводы в пользу того, почему вы можете захотеть использовать одно вместо другого, но ничего достаточно существенного, что оправдывало бы риск.

Как исправить…

Есть как минимум два способа, с помощью которых вы можете поправить свой код, чтобы избежать зависаний. Первый метод предпочтительнее, так как вы не будете блокировать поток UI. Второй метод менее оптимальный, так как вы по-прежнему будете блокировать, но на меньшее время, чем, скорее всего, вы делаете это сейчас.

  1. Пишите свой код так, чтобы запросы были асинхронными

    Вам нужно передать значение true для bAsync параметра в методе open(), и вам также нужно будет написать обработчик для события onreadystatechange. В интернете есть множество примеров, как это можно сделать, например, вот этот.
  2. Установите свойство timeout.

    Так как Desktop Window Manager обнаруживает зависание через 5 секунд без ответа, а восстановление после зависания в IE срабатывает через 8, я рекомендую устанавливать timeout-свойство в не более, чем 5 секунд. Также, возможно, вы захотите поставить обработчик для реакции на событие ontimeout. (Откуда я знаю эти числа? Читайте подробнее здесь и здесь)

Как насчет примера?

Давайте посмотрим, как это все работает, детальнее, на простом сценарии:

  1. Мы напишем код, который отсылает синхронный запрос и этим подвешивает браузер, и далее
  2. заменим код на асинхронный и посмотрим на изменения в поведении браузера.

Цель этого сценария – показать, как переход от синхронного запроса к асинхронному запросу улучшает впечатления пользователя от работы с вашим приложением.

ПРЕДУПРЕЖДЕНИЕ: Этот пример подвесит ваш браузер и может привести к непредсказуемым и даже нежелаемым результатам как на стороне клиента, так и на стороне сервера. Другими словами, не запускаете его на продакшн-сервере или любой другой машине, которая не может корректно отработать нестабильный и потенциально неправильный код.

Установка

Вам понадобится сервер, способный запускать ASP.NET страницы, если вы хотите повторить непосредственно пример, приведенный ниже. Я понимаю, что, возможно, не все читатели используют ASP.NET, но код должен быть достаточно прозрачным, чтобы вы могли без проблем воспроизвести его в другом окружении.

Также я предполагаю, что вы знакомы с работой веб-сервера и знаете, как запускать страницы с серверной логикой.

Скопируйте этот кода в ваш любимый текстовый редактор и сохраните его как hangme.aspx где-нибудь на вашем сервере.

<!-- code starts after this line --> 

<%@ Page Language="C#" %>

<html>

<head>

    <title>XmlHttpRequest open hang test</title>

    <script runat="server">

        protected void Page_Load(object sender, EventArgs e) {

            if(Request.QueryString["hang"] == "1") {

                int seconds = 0;

                Int32.TryParse(Request.QueryString["seconds"], out seconds);

                System.Threading.Thread.Sleep(seconds * 1000);

            }

        }

    </script>

    <script type="text/javascript">

        function call_hangme() {

            var oReq;

            if (window.XMLHttpRequest) {

                oReq = new XMLHttpRequest();

            }

            if (oReq != null) {

                var sUrl = "http://localhost/hangme.aspx?hang=1&seconds=360";

                // change localhost to your server name if applicable

                document.getElementById("txt1").innerHTML = "Request Sent...";

                // pass false for the bAsync parameter for a synchronous request

                oReq.open("GET", sUrl, false);

                oReq.send();

                document.getElementById("txt1").innerHTML = "Response Received!";

            }

            else {

                window.alert("Error creating XmlHttpRequest object.");

            }

        } 

    </script>

</head>

<body>

    <p>Click this button to hang the browser</p>

    <form name="form1" action="" method="get">

        <input type="button" name="btn1" value="hang me" onClick="call_hangme()">

    </form>

    <p id="txt1"/>  

</body>

</html> 

<!-- code ends on the line before this one --> 

Давайте его подвесим!

Теперь,

  1. Откройте Internet Explorer и перейдите на страницу hangme.aspx.
  2. Нажмите на кнопку “hang me”.

Ух-ох! Мы зависли! Так как мы сказали потоку поспать 6 минут, нам придется тут немного поторчать. Если вы используете IE9, вы наверняка, заметите, золотистую плашку снизу, предлагающую “Восстановить страницу”. Если вы используете другой браузер, скорее всего, вы получите замороженное окно.

Используем асинхронное открытие запроса

Скопируйте этот код в новый файл и сохраните его как wont_hangme.aspx.

<!-- code starts after this line --> 

<%@ Page Language="C#" %>

<html>

<head>

    <title>XmlHttpRequest open hang test</title>

    <script runat="server">

        protected void Page_Load(object sender, EventArgs e) {

            if(Request.QueryString["hang"] == "1") {

                int seconds = 0;

                Int32.TryParse(Request.QueryString["seconds"], out seconds);

                System.Threading.Thread.Sleep(seconds * 1000);

            }

        }

    </script>

    <script type="text/javascript">

        function call_hangme() {

            var oReq;

            if (window.XMLHttpRequest) {

                oReq = new XMLHttpRequest();

            }

            if (oReq != null) {

                var sUrl = "http://localhost/wont_hangme.aspx?hang=1&seconds=360";

                // change localhost to your server name if applicable

                document.getElementById("txt1").innerHTML = "Request Sent...";

                // pass true for the bAsync parameter 

                oReq.open("GET", sUrl, true);

                

                /* Here we define an anonymous function for the

                onreadystatechange event.  We check if we received all the data,  

                and the request was successful before changing the text */

                oReq.onreadystatechange = function() {

                    if (oReq.readyState == 4 && oReq.status == 200) {

                        document.getElementById("txt1").innerHTML = "Response Received!";

                    }

                }

                

                oReq.send();

            }

            else {

                window.alert("Error creating XmlHttpRequest object.");

            }

        } 

    </script>

</head>

<body>

    <p>Click this button to hang the browser</p>

    <form name="form1" action="" method="get">

        <input type="button" name="btn1" value="hang me" onClick="call_hangme()">

    </form>

    <p id="txt1"/>  

</body>

</html> 

<!-- code ends on the line before this one --> 

Новый код подсвечен. Здесь мы теперь вызываем метод open() с параметром bAsync = trur, что означает, что мы делаем асинхронный вызов. Это освободит поток UI для других задач вместо того, чтобы ждать ответа или таймаута. Если мы не заблокированы, мы можем пересылать сообщением, а если так, то мы не зависли!

Теперь, когда ответ от сервера рано или поздно приходит к нам назад, нам понадобится функция, чтобы его обработать. Именно это и делает подсвеченный код. Это наш обработчик для события onreadystatechange. Как вы можете видеть, я всего лишь проверяю, что запрос успешно прошел (status == 200) и что все данные были получены (readyState == 4).

Можем мы подвесить еще раз?

Снова,

  1. Откройте Internet Explorer и перейдите на страницу wont_hangme.aspx.
  2. Нажмите кнопку “hang me”.

Voilà! Как вы видите, запрос отправлен, но браузер не завис. Когда ответ все-таки придет назад к нам, тект должен поменяться; но как бы там ни было, вы по-прежнему можете взаимодействовать со страницей.

В заключение

Как было сказано в самом начале, вам следует делать ваши XMLHttpRequest запросы асинхронными. Этот пример четко показывает, почему именно. Вы повысите отзывчивость ваших страниц, сможете избежать подвисаний, и, что самое главное, обеспечите для ваших пользователей более приятное взаимодействие с вашими приложениями.

Source: Why You Should Use XMLHttpRequest Asynchronously