Mejora del rendimiento de la aplicación de almacenamiento en caché del controlador de eventos y React Application

Hoy publicamos una traducción del material, cuyo autor, después de analizar las características de trabajar con objetos en JavaScript, ofrece a los desarrolladores de React una metodología para acelerar las aplicaciones. En particular, estamos hablando del hecho de que una variable, a la que, como dicen, se le "asigna un objeto", y que a menudo se llama simplemente "objeto", de hecho, no almacena el objeto en sí, sino un enlace a él. Las funciones en JavaScript también son objetos, por lo que lo anterior es cierto para ellos. Teniendo esto en cuenta, el diseño de componentes React y el análisis crítico de su código pueden mejorar sus mecanismos internos y mejorar el rendimiento de la aplicación.



Características de trabajar con objetos en JavaScript


Si crea un par de funciones que se ven exactamente iguales e intenta compararlas, resulta que, desde el punto de vista del sistema, son diferentes. Para verificar esto, puede ejecutar el siguiente código:

const functionOne = function() { alert('Hello world!'); }; const functionTwo = function() { alert('Hello world!'); }; functionOne === functionTwo; // false 

Ahora intentemos asignar una variable a una función existente que ya está asignada a otra variable, y compare estas dos variables:

 const functionThree = function() { alert('Hello world!'); }; const functionFour = functionThree; functionThree === functionFour; // true 

Como puede ver, con este enfoque, el operador de igualdad estricta devuelve true .
Los objetos se comportan naturalmente de la misma manera:

 const object1 = {}; const object2 = {}; const object3 = object1; object1 === object2; // false object1 === object3; // true 

Aquí estamos hablando de JavaScript, pero si tiene experiencia en el desarrollo en otros idiomas, entonces puede estar familiarizado con el concepto de punteros. En el código anterior, cada vez que se crea un objeto, se le asigna una parte de la memoria del sistema. Cuando usamos un comando de la forma object1 = {} , esto lleva a llenar con algunos datos una pieza de memoria asignada específicamente para object1 .

Es bastante posible imaginar el object1 como la dirección en la que las estructuras de datos relacionadas con el objeto se encuentran en la memoria. La ejecución del comando object2 = {} conduce a la asignación de otra área de memoria, diseñada específicamente para object2 . ¿Están obect1 y object2 en la misma área de memoria? No, cada uno de ellos tiene su propia trama. Es por eso que cuando intentamos comparar object1 y object2 obtenemos false . Estos objetos pueden tener una estructura idéntica, pero las direcciones en la memoria donde están ubicadas difieren, y son las direcciones las que se verifican durante la comparación.

Al ejecutar el comando object3 = object1 , escribimos la dirección de object1 en object3 constante. Este no es un objeto nuevo. A esta constante se le asigna la dirección de un objeto existente. Puede verificar esto mediante:

 const object1 = { x: true }; const object3 = object1; object3.x = false; object1.x; // false 

En este ejemplo, se crea un objeto en la memoria y su dirección se escribe en el objeto constante1. Luego se escribe la misma dirección en el objeto constante3. El cambio de object3 cambia el objeto en la memoria. Esto significa que al acceder a un objeto utilizando cualquier otra referencia a él, por ejemplo, el que está almacenado en object1 , ya trabajaremos con su versión modificada.

Funciones, objetos y reaccionar


El malentendido del mecanismo anterior por parte de los desarrolladores novatos a menudo conduce a errores, y, tal vez, la consideración de las características de trabajar con objetos merece un artículo separado. Sin embargo, nuestro tema hoy es el rendimiento de las aplicaciones React. En esta área, incluso los desarrolladores bastante experimentados pueden cometer errores y simplemente no prestan atención a cómo las aplicaciones React se ven afectadas por el hecho de que las variables y constantes de JavaScript no se almacenan en los objetos, sino que solo se vinculan con ellas.

¿Qué tiene esto que ver con React? React tiene mecanismos inteligentes para ahorrar recursos del sistema destinados a mejorar el rendimiento de la aplicación: si las propiedades y el estado del componente no cambian, entonces la función de render no cambia. Obviamente, si el componente sigue siendo el mismo, no necesita ser renderizado nuevamente. Si nada cambia, la función de render devolverá lo mismo que antes, por lo que no hay necesidad de ejecutarla. Este mecanismo hace que React sea rápido. Algo se muestra solo cuando es necesario.

React verifica la igualdad de las propiedades y el estado de los componentes utilizando las características estándar de JavaScript, es decir, simplemente las compara con el operador == React no realiza una comparación "superficial" o "profunda" de los objetos para determinar su igualdad. Una comparación superficial es un concepto utilizado para describir una comparación de cada par clave-valor de un objeto, en oposición a una comparación en la que solo se comparan las direcciones de los objetos en la memoria (referencias a ellos). La comparación "profunda" de los objetos va más allá, y si los valores de las propiedades comparadas de los objetos también son objetos, también comparan los pares clave-valor de estos objetos. Este proceso se repite para todos los objetos anidados en otros objetos. React no hace nada por el estilo, solo verifica la igualdad de enlaces.

Si, por ejemplo, cambia la propiedad de un componente representado por un objeto de la forma { x: 1 } a otro objeto que se ve exactamente igual, React volverá a representar el componente, ya que estos objetos se encuentran en diferentes áreas de memoria. Si recuerda el ejemplo anterior, cuando cambie las propiedades de un componente de object1 a object3 , React no volverá a representar dicho componente, ya que las constantes object1 y object3 refieren al mismo objeto.

Trabajar con funciones en JavaScript se organiza exactamente de la misma manera. Si React encuentra las mismas características cuyas direcciones son diferentes, se volverá a procesar. Si la "nueva función" es solo un enlace a una función que ya se ha utilizado, no habrá representación nueva.

Un problema típico cuando se trabaja con componentes.


Este es uno de los escenarios de trabajo con componentes, que, desafortunadamente, constantemente se me ocurre cuando verifico el código de otra persona:

 class SomeComponent extends React.PureComponent { get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={() => alert('!')} />     </div>   ); } } 

Ante nosotros hay un componente muy simple. Es un botón, cuando se hace clic, se muestra una notificación. Junto al botón se muestran instrucciones para su uso, informando al usuario si debe presionar este botón. Controle qué instrucción se mostrará configurando la SomeComponent do ( do={true} o do={false} ) SomeComponent .

Cada vez que el componente SomeComponent se vuelve a representar (cuando el valor de la propiedad do se cambia de true a false y viceversa), también se representa el elemento Button . El controlador onClick , aunque siempre es el mismo, se vuelve a crear cada vez render se llama a la función de representación. Como resultado, resulta que cada vez que el componente se muestra en la memoria, se crea una nueva función, ya que su creación se realiza en la función de render , se pasa un enlace a la nueva dirección en la memoria a <Button /> , y el componente del Button también se vuelve a representar, a pesar de que en nada ha cambiado en absoluto.

Hablemos sobre cómo solucionarlo.

Resolución de problemas


Si la función es independiente del componente ( this contexto), puede definirla fuera del componente. Todas las instancias del componente utilizarán la misma referencia de función, ya que en todos los casos será la misma función. Así es como se ve:

 const createAlertBox = () => alert('!'); class SomeComponent extends React.PureComponent { get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={createAlertBox} />     </div>   ); } } 

A diferencia del ejemplo anterior, createAlertBox , con cada llamada a render , contendrá el mismo enlace a la misma área en la memoria. Como resultado, Button salida repetida Button no se ejecutará.

Si bien el componente Button es pequeño y se procesa rápidamente, el problema anterior asociado con la declaración interna de funciones también se puede encontrar en componentes grandes y complejos que tardan mucho tiempo en procesarse. Esto puede ralentizar significativamente la aplicación React. En este sentido, tiene sentido seguir la recomendación, según la cual dichas funciones nunca deberían declararse dentro del método de render .

Si la función depende del componente, es decir, no se puede definir fuera de él, el método del componente se puede pasar como un controlador de eventos:

 class SomeComponent extends React.PureComponent { createAlertBox = () => {   alert(this.props.message); }; get instructions() {   if (this.props.do) {     return 'Click the button: ';   }   return 'Do NOT click the button: '; } render() {   return (     <div>       {this.instructions}       <Button onClick={this.createAlertBox} />     </div>   ); } } 

En este caso, en cada instancia de SomeComponent cuando haga clic en el botón, se mostrarán varios mensajes. El controlador de eventos del elemento Button debe ser exclusivo de SomeComponent . Al pasar el método cteateAlertBox , no importa si SomeComponent vuelve a representar. No importa si la propiedad del message ha cambiado. La dirección de la función createAlertBox no cambia, lo que significa que el elemento Button no se debe volver a representar. Gracias a esto, puede ahorrar recursos del sistema y mejorar la velocidad de representación de la aplicación.

Todo esto esta bien. Pero, ¿y si las funciones son dinámicas?

Resolviendo un problema más complejo


El autor de este material le pide que preste atención al hecho de que preparó los ejemplos en esta sección, tomando lo primero que le vino a la mente, adecuado para ilustrar la reutilización de funciones. Estos ejemplos están destinados a ayudar al lector a comprender la esencia de la idea. Aunque esta sección se recomienda para leer para comprender la esencia de lo que está sucediendo, el autor aconseja prestar atención a los comentarios sobre el artículo original , ya que algunos lectores han sugerido mejores versiones de los mecanismos discutidos aquí, que tienen en cuenta las características de invalidación de caché y mecanismos de administración de memoria integrados en React.

Por lo tanto, es extremadamente común que en un componente haya muchos controladores de eventos dinámicos únicos, por ejemplo, se puede ver algo similar en el código, donde se usa el método de matriz de map en el método de render :

 class SomeComponent extends React.PureComponent { render() {   return (     <ul>       {this.props.list.map(listItem =>         <li key={listItem.text}>           <Button onClick={() => alert(listItem.text)} />         </li>       )}     </ul>   ); } } 

Aquí, se mostrará un número diferente de botones y se creará un número diferente de controladores de eventos, cada uno de los cuales está representado por una función única y, por adelantado, al crear SomeComponent , no se sabe cuáles serán estas funciones. ¿Cómo resolver este rompecabezas?

Aquí la memorización nos ayudará o, más simplemente, el almacenamiento en caché. Para cada valor único, cree una función y póngala en la memoria caché. Si este valor único ocurre nuevamente, será suficiente para tomar de la memoria caché la función que le corresponde, que previamente se colocó en la memoria caché.

Así es como se ve la implementación de esta idea:

 class SomeComponent extends React.PureComponent { //    SomeComponent        //   . clickHandlers = {}; //       //    . getClickHandler(key) {   //       ,  .   if (!Object.prototype.hasOwnProperty.call(this.clickHandlers, key)) {     this.clickHandlers[key] = () => alert(key);   }   return this.clickHandlers[key]; } render() {   return (     <ul>       {this.props.list.map(listItem =>         <li key={listItem.text}>           <Button onClick={this.getClickHandler(listItem.text)} />         </li>       )}     </ul>   ); } } 

Cada elemento de la matriz se procesa mediante el método getClickHandler . Este método, la primera vez que se llama con un cierto valor, creará una función única para este valor, lo colocará en el caché y lo devolverá. Todas las llamadas posteriores a este método, al pasarle el mismo valor, harán que simplemente devuelva un enlace a la función desde el caché.

Como resultado, la representación de SomeComponent no volverá a representar el Button . Del mismo modo, agregar elementos a la propiedad de la list creará dinámicamente controladores de eventos para cada botón.

Deberá ser creativo al crear identificadores únicos para los controladores si están definidos por más de una variable, pero esto no es mucho más complicado que la creación habitual de una propiedad de key única para cada objeto JSX obtenido como resultado del método de map .

Aquí me gustaría advertirle sobre posibles problemas de uso de índices de matriz como identificadores. El hecho es que con este enfoque, puede encontrar errores si cambia el orden de los elementos en la matriz o si se eliminan algunos de sus elementos. Entonces, por ejemplo, si al principio una matriz de este tipo se parecía a [ 'soda', 'pizza' ] , y luego se convertía en [ 'pizza' ] , y almacenaba en caché los controladores de eventos usando un comando de los listeners[0] = () => alert('soda') formularios listeners[0] = () => alert('soda') , encontrará que cuando el usuario hace clic en el botón al que está asignado el controlador con el identificador 0 y que, de acuerdo con el contenido de la matriz [ 'pizza' ] , debe mostrar un mensaje de pizza , se mostrará un mensaje de soda . Por la misma razón, no se recomienda usar índices de matriz como propiedades clave.

Resumen


En este artículo, examinamos las características de los mecanismos internos de JavaScript y consideramos que puede acelerar la representación de las aplicaciones React. Esperamos que las ideas presentadas aquí sean útiles.

Estimados lectores! Si conoce alguna forma interesante de optimizar las aplicaciones React, compártalas.

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


All Articles