Rendimiento y tramas de JavaScript
Hace años, ASP.NET nos dio la representación del control de interfaz de usuario del lado servidor, y funcionaba. Sin embargo, esta representación del lado servidor, requiere código de plena confianza. Ahora que se ha realizado la transición a SharePoint y Office 365, el código de plena confianza ya no es una opción. Eso significa que la representación del control de interfaz de usuario del lado servidor ya no funcionará.
Pero las empresas aún necesitan una funcionalidad de interfaz de usuario personalizada para sus sitios web y aplicaciones. Eso significa que la funcionalidad de interfaz de usuario personalizada debe moverse desde el lado servidor al lado cliente.
Ahora, JavaScript del lado cliente es la mejor opción para la representación de controles de la interfaz de usuario.
Patrones de JavaScript
Puesto que el lado cliente de JavaScript es la ruta de acceso, ¿cuáles son las mejores formas de implementar el lado cliente de JavaScript? ¿Cómo empezar?
Existen varias opciones:
Opción | Descripción |
---|---|
Inserción de JavaScript | Site.UserCustomActions o Web.UserCustomActions permiten la inclusión del script directamente en la marcación de la página. Se usa en el patrón cargador descrito a continuación |
Plantillas para mostrar | Se aplica a las vistas y la búsqueda. No tiene que implementar ningún tipo de aplicación o código hospedado por el proveedor. Es simplemente un archivo de JavaScript que puede cargarse en (por ejemplo), la biblioteca de estilos para personalizar las vistas. Puede crear cualquier vista necesaria mediante JavaScript |
Complementos hospedados de SharePoint | Utiliza JSOM para comunicarse con la web del host o del complemento. Proporciona acceso al Proxy Web para llamadas de dominio cruzadas |
Complementos hospedados por el proveedor | Permite la creación de aplicaciones complejas en una variedad de pilas de tecnología, al tiempo que mantiene una integración segura con SharePoint |
JSLink | Le permite cargar uno o más archivos JavaScript en varios elementos web OOTB y vistas |
Elemento web ScriptEditor | Incluir script directamente o cargado por etiquetas de script con la marcación para crear aplicaciones de página única complejas hospedadas por completo en el sitio de SharePoint |
No crea que está limitado a estas opciones si piensa que otra opción sería mejor para su situación.
Rendimiento de JavaScript
En cada paso del proceso de desarrollo, es importante tener en cuenta el rendimiento. Hay algunas cosas que tienen un gran impacto en el rendimiento de JavaScript:
Opción | Descripción |
---|---|
Reducir el número de solicitudes | Menos solicitudes significa menos viajes de ida y vuelta al servidor, por lo que reduce la latencia. |
Recuperar solo los datos que necesita | Reduce la cantidad de datos que se envían por la red. También reduce la carga del servidor. |
Proporcionar una buena experiencia de carga de página | Mantiene la interfaz de usuario en correcto funcionamiento. Por ejemplo, actualice los menús en la página antes de iniciar la descarga de más de 100 registros. |
Usar patrones y llamadas asincrónicas siempre que sea posible | El sondeo es una carga más pesada para el rendimiento que una devolución de llamada o una llamada asincrónica. |
El almacenamiento en caché es clave | El almacenamiento en caché reduce aún más la carga en el servidor a l vez que mejora el rendimiento de inmediato. |
Prepararse para más vistas de páginas de las que podría imaginar | Una página de aterrizaje con gran cantidad de datos es aceptable si solo tiene algunas visitas. Pero si recibe miles de visitas, esto puede afectar al rendimiento. |
Qué hace el código
Para el rendimiento, es importante saber qué está haciendo el código en todo momento. Esto le permite identificar formas de mejorar la eficiencia. A continuación se muestran algunas formas útiles de hacerlo.
Reducir el número de solicitudes
Haga siempre el menor número de solicitudes y lo más pequeñas posible. Cada solicitud que elimina reduce la carga de rendimiento en el servidor y en el cliente. Y hacer solicitudes más pequeñas reduce aún más la carga de rendimiento.
Hay varias maneras de reducir el tamaño y el número de las solicitudes.
- Limitar el número de archivos de JavaScript en producción. Separar los archivos de JavaScript funciona bien para el desarrollo, pero no tan bien para la producción. Combine los archivos de JavaScript en un solo archivo de JavaScript o en el menor número posible de archivos de JavaScript.
- Reducir el tamaño de los archivos. Minimice los archivos de JavaScript de producción quitando saltos de línea, espacios en blanco y comentarios. Hay varios programas y sitios web para minificar archivos JavaScript que puede usar para reducir en gran medida el tamaño de archivo de JavaScript.
- Use el almacenamiento en caché de archivos del explorador para reducir las solicitudes. El patrón cargador actualizado a continuación es un buen método para ampliar esta idea.
Recuperar solo los datos que necesita
Cuando solicite datos, recuerde centrar las solicitudes en lo que realmente necesita. Descargar un artículo entero para obtener el título, por ejemplo, reducirá bastante el rendimiento.
- Use el filtrado del servidor, la selección y los límites para minimizar el tráfico en la red.
- O no solicite todos los artículos si quiere solo los cinco primeros.
- No solicite el contenedor de propiedades completo si solo quiere una propiedad. En un ejemplo, un script necesitaba sólo una propiedad, pero solicitó la bolsa de propiedades completa, lo que resultó ser 800 KB. Recuerde también que el tamaño de un objeto puede cambiar con el tiempo, por lo que algo que ahora solo ocupa unos cuantos kilobytes puede convertirse en megabytes más adelante en el ciclo de vida del producto.
No solicitar datos que se descartarán sin usarse
Si recupera más datos de los que usa, considérelo una oportunidad para filtrar mejor la consulta inicial.
- Solicite solo los campos que necesita, como Nombre y Dirección, no todo el registro.
- Haga solicitudes de filtro deliberadas y específicas. Por ejemplo, si desea que aparezcan los artículos disponibles, obtenga la fecha de publicación, el título y el autor. Deje el resto de los campos fuera de la solicitud.
Proporcionar una buena experiencia de usuario
Las interfaces de usuario incoherentes e irregulares afectan no solo al rendimiento, sino también a la percepción del rendimiento. Escriba el código de manera que ofrezca una experiencia fluida.
- Use un indicador giratorio para indicar que las cosas están cargándose o tardando.
- Comprenda el orden de ejecución del código, y ajústelo para la mejor experiencia de usuario. Por ejemplo, si quiere recuperar una gran cantidad de datos del servidor y quiere cambiar la interfaz de usuario ocultando un menú, oculte primero el menú. Evitará que una experiencia escalonada de la interfaz de usuario para el usuario.
Todo es asincrónico
Toda actividad de código en el explorador debe considerarse asincrónica. Los archivos se cargan en un orden, debe esperar a cargar el DOM y las solicitudes a SharePoint se completará a diferentes velocidades.
- Comprenda cómo funciona el código en el tiempo.
- Use eventos y devoluciones de llamada en lugar de sondeos.
- Use promesas. En jQuery se llaman objetos Deferred. Hay conceptos similares en Q, WinJS y ES6.
- Utilice la llamada asincrónica en lugar de la llamada no asincrónica.
- Use la forma asincrónica cada vez que pueda haber un retraso:
- Durante una solicitud de AJAX.
- Durante cualquier manipulación de DOM importante.
Los patrones asincrónicos mejoran el rendimiento y la capacidad de respuesta y permiten el encadenamiento eficaz de acciones dependientes.
Almacenamiento en caché del lado cliente
El almacenamiento en caché del lado cliente es una de las mejoras de rendimiento que puede agregar a su código que suele olvidarse a menudo.
Hay tres lugares diferentes en los que puede almacenar en caché los datos:
Opción | Descripción |
---|---|
Almacenamiento de sesión | Almacena datos como un par clave-valor en el cliente. Esto es por almacenamiento de sesión, que siempre se almacena como cadenas. JSON.stringify() convertirá los objetos de JavaScript en cadenas que ayudan a almacenar objetos. |
Almacenamiento local | Almacena datos como un par clave-valor en el cliente. Esto es persistente en todas las sesiones, siempre se almacena como cadenas. JSON.stringify() convertirá los objetos de JavaScript en cadenas que ayudan a almacenar objetos. |
Base de datos local | Almacena datos relacionales en el cliente. Con frecuencia usa SQL-Lite como el motor de base de datos. El almacenamiento de base de datos local no siempre está disponible en todos los exploradores: compruebe la compatibilidad con el explorador de destino. |
Al almacenar en caché, tenga en cuenta los límites de almacenamiento disponibles y cómo de actuales son los datos.
- Si está alcanzando l final de los límites de almacenamiento, podría ser conveniente quitar los datos antiguos o menos importantes de la caché.
- Algunos tipos de datos pueden pasar a ser obsoletos más rápido que otros. Una lista de artículos de noticias puede quedar obsoleta en cinco o diez minutos, pero el nombre de perfil de usuario a menudo puede guardarse en caché de forma segura durante 24 horas o más.
El almacenamiento local y el almacenamiento de sesión no tienen la expiración integrada, pero las cookies sí. Puede asociar los datos almacenados a una cookie para añadir una expiración al almacenamiento local y de la sesión. También puede crear un contenedor de almacenamiento que incluya una fecha de expiración y añadirlo en el código.
El precio de la popularidad
¿Con qué frecuencia se ve la página? En el escenario clásico, la página principal corporativa se establece como la página de inicio de todos los exploradores en toda la organización. De repente obtendrá mucho más tráfico del que nunca imaginó. De repente se magnifica cada byte de contenido en el rendimiento del servidor y el ancho de banda que usa la página principal.
La solución: haga una página principal ligera e incluya vínculos para el resto contenido.
Los paneles con gran cantidad de datos también son válidos para una aplicación hospedada por el proveedor que puede escalar de forma independiente.
El patrón cargador
El objetivo del patrón cargador es ofrecer una forma de insertar un número desconocido de scripts remotos en un sitio sin tener que actualizar el sitio. Las actualizaciones se pueden realizar en la red CDN remota y actualizarán todos los sitios.
El patrón cargador crea una URL con la marca de fecha y hora al final de manera que el archivo no se almacenará en caché. Configura jQuery como una dependencia en el archivo cargador, después ejecuta una función en el cargador. Esto asegura que el JavaScript personalizado cargará cuando finalice la carga de jQuery.
PnP-dev\Samples\Core.JavaScript\Core.JavaScript.Embedder\Program.cs:
static void Main(string[] args)
{
ContextManager.WithContext((context) =>
// this is the script block that will be embedded into the page
// in practice this can be done during provisioning of the site/web
// make sure to include ';' at end to play nice with page embedding
// using the script on demand feature built into SharePoint we load jQuery, then our remote loader(pnp-loader.js or pnp-loader-cached.js) file using a dependency
var script = @"(function (loaderFile, nocache) {
var url = loaderFile + ((nocache) ? '?' + encodeURIComponent((new Date()).getTime()) : '');
SP.SOD.registerSod('pnp-jquery.js', 'https://localhost:44324/js/jquery.js');
SP.SOD.registerSod('pnp-loader.js', url);
SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js');
SP.SOD.executeFunc('pnp-loader.js', null, function() {});
})('https://localhost:44324/pnp-loader.js', true);";
// this version of the script along with pnp-loaderMDS.js (or pnp-loaderMDS-cached.js) handles pages where the minimum download strategy is active
var script2 = @"ExecuteOrDelayUntilBodyLoaded(function () {
var url = 'https://localhost:44324/js/pnp-loaderMDS.js?' + encodeURIComponent((new Date()).getTime());
SP.SOD.registerSod('pnp-jquery.js', 'https://localhost:44324/js/jquery.js');
SP.SOD.registerSod('pnp-loader.js', url);
SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js');
SP.SOD.executeFunc('pnp-loader.js', null, function () {
if (typeof pnpLoadFiles === 'undefined') {
RegisterModuleInit('https://localhost:44324/js/pnp-loaderMDS.js', pnpLoadFiles);
} else {
pnpLoadFiles();
}
});
});";
// load the collection of existing links
var links = context.Site.RootWeb.UserCustomActions;
context.Load(links, ls => ls.Include(l => l.Title));
context.ExecuteQueryRetry();
// this block handles deleting previous test custom actions
var doDelete = false;
foreach (var link in links.ToArray().Where(l => l.Title.Equals("MyTestCustomAction", StringComparison.OrdinalIgnoreCase)))
{
link.DeleteObject();
doDelete = true;
}
if (doDelete)
{
context.ExecuteQueryRetry();
}
// now we embed our script into the user custom action
var newLink = context.Site.RootWeb.UserCustomActions.Add();
newLink.Title = "MyTestCustomAction";
newLink.Description = "Doing some testing.";
newLink.ScriptBlock = script2;
newLink.Location = "ScriptLink";
newLink.Update();
context.ExecuteQueryRetry();
});
}
La SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js');
configura la dependencia, y SP.SOD.executeFunc('pnp-loader.js', null, function() {});
fuerza a jQuery para que cargue completamente antes de cargar el JavaScript personalizado.
El newLink.ScriptBlock = script2;
y el newLink.Location = "ScriptLink";
son las partes principales para agregar esto en la acción de cliente del usuario.
El archivo pnp-loader.js después carga una lista de archivos JavaScript, con un promesa que se puede ejecutar cuando se carga cada archivo.
PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-loader.js:
(function () {
var urlbase = 'https://localhost:44324';
var files = [
'/js/pnp-settings.js',
'/js/pnp-core.js',
'/js/pnp-clientcache.js',
'/js/pnp-config.js',
'/js/pnp-logging.js',
'/js/pnp-devdashboard.js',
'/js/pnp-uimods.js'
];
// create a promise
var promise = $.Deferred();
// this function will be used to recursively load all the files
var engine = function () {
// maintain context
var self = this;
// get the next file to load
var file = self.files.shift();
var fullPath = urlbase + file;
// load the remote script file
$.getScript(fullPath).done(function () {
if (self.files.length > 0) {
engine.call(self);
}
else {
self.promise.resolve();
}
}).fail(self.promise.reject);
};
// create our "this" we will apply to the engine function
var ctx = {
files: files,
promise: promise
};
// call the engine with our context
engine.call(ctx);
// give back the promise
return promise.promise();
})().done(function () {
/* all scripts are loaded and I could take actions here */
}).fail(function () {
/* something failed, take some action here if needed */
});
El archivo pnp-loader.js no almacena en caché, lo que funciona bien en un entorno de desarrollo. El archivo pnp-loader.js reemplaza la función $.getScript
con una función $.ajax
que permite almacenar en caché del explorador los archivos y es más adecuado para la producción.
Desde PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-loader.js
// load the remote script file
$.ajax({
type: 'GET',
url: fullPath,
cache: true,
dataType: 'script'
}).done(function () {
if (self.files.length > 0) {
engine.call(self);
}
else {
self.promise.resolve();
}
}).fail(self.promise.reject);
Este patrón facilita la implementación y las actualizaciones de sitios. Resulta especialmente útil al implementar o actualizar en miles de colecciones de sitios.
Almacenamiento en caché del usuario actual
Si ya está almacenada en caché la información de usuario, esta función obtiene los datos de la caché de sesión. Si la información de usuario no está almacenada en la memoria caché, obtiene la información del usuario específico que necesita y la almacena en la caché.
También usa Deferred (versión jQuery de promesa). Si se obtienen los datos de la memoria caché o del servidor, el Deferred se resuelve. Si hay un error, se rechazará el Deferred.
Desde PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-core.js:
getCurrentUserInfo: function (ctx) {
var self = this;
if (self._currentUserInfoPromise == null) {
self._currentUserInfoPromise = $.Deferred(function (def) {
var cachingTest = $pnp.session !== 'undefined' && $pnp.session.enabled;
// if we have the caching module loaded
if (cachingTest) {
var userInfo = $pnp.session.get(self._currentUserInfoCacheKey);
if (userInfo !== null) {
self._currentUserInfo = userInfo;
def.resolveWith(ctx || self._currentUserInfo, [self._currentUserInfo]);
return;
}
}
// send the request and allow caching
$.ajax({
method: 'GET',
url: '/_api/SP.UserProfiles.PeopleManager/GetMyProperties?$select=AccountName,DisplayName,Title',
headers: { "Accept": "application/json; odata=verbose" },
cache: true
}).done(function (response) {
// we also parse and add some custom properties as an example
self._currentUserInfo = $.extend(response.d,
{
ParsedLoginName: $pnp.core.getUserIdFromLogin(response.d.AccountName)
});
if (cachingTest) {
$pnp.session.add(self._currentUserInfoCacheKey, self._currentUserInfo);
}
def.resolveWith(ctx || self._currentUserInfo, [self._currentUserInfo]);
}).fail(function (jqXHR, textStatus, errorThrown) {
console.error('[PNP]=>[Fatal Error] Could not load current user data data from /_api/SP.UserProfiles.PeopleManager/GetMyProperties. status: ' + textStatus + ', error: ' + errorThrown);
def.rejectWith(ctx || null);
});
});
}
return this._currentUserInfoPromise.promise();
}
}
Patrón de almacenamiento en caché con Deferred y asincrónico
Otro patrón de almacenamiento en caché puede encontrarse en pnp-clientcache.js storageTest, que procede del storageTest del modernizador. Contiene funciones para obtener, agregar, quitar y getOrAdd que devolverán los datos almacenados en caché, si está en la caché, o recuperarán los datos del servidor y lo añadirán a la caché, si no está en la caché, lo que le ahorra escribir código repetitivo en la función de llamada. get usa JSON.parse para probar la expiración, ya que la expiración no es una característica del almacenamiento local. _createPersistable almacena el objeto JavaScript en la caché de almacenamiento local.
Desde PnP-dev\Samples\Core.JavaScript\Core.JavaScript.CDN\js\pnp-clientcache.js:
// adds the client cache capability
caching: {
// determine if we have local storage once
enabled: storageTest(),
add: function (/*string*/ key, /*object*/ value, /*datetime*/ expiration) {
if (this.enabled) {
localStorage.setItem(key, this._createPersistable(value, expiration));
}
},
// gets an item from the cache, checking the expiration and removing the object if it is expired
get: function (/*string*/ key) {
if (!this.enabled) {
return null;
}
var o = localStorage.getItem(key);
if (o == null) {
return o;
}
var persistable = JSON.parse(o);
if (new Date(persistable.expiration) <= new Date()) {
this.remove(key);
o = null;
} else {
o = persistable.value;
}
return o;
},
// removes an item from local storage by key
remove: function (/*string*/ key) {
if (this.enabled) {
localStorage.removeItem(key);
}
},
// gets an item from the cache or adds it using the supplied getter function
getOrAdd: function (/*string*/ key, /*function*/ getter) {
if (!this.enabled) {
return getter();
}
if (!$.isFunction(getter)) {
throw 'Function expected for parameter "getter".';
}
var o = this.get(key);
if (o == null) {
o = getter();
this.add(key, o);
}
return o;
},
// creates the persisted object wrapper using the value and the expiration, setting the default expiration if none is applied
_createPersistable: function (/*object*/ o, /*datetime*/ expiration) {
if (typeof expiration === 'undefined') {
expiration = $pnp.core.dateAdd(new Date(), 'minute', $pnp.settings.localStorageDefaultTimeoutMinutes);
}
return JSON.stringify({
value: o,
expiration: expiration
});
}
},
Para un uso más complejo de Deferred y asincrónico, puede hacer consultar el panel de desarrollador en pnp-clientcache.js
Recursos
Consulta también
- Consideraciones de rendimiento en el modelo de complemento de SharePoint
- Completar operaciones básicas con código de biblioteca de JavaScript en SharePoint 2013
- Ejemplos de código de representación del lado cliente (vínculo JS)
- Inserción de JavaScript (personalizar su interfaz de usuario del sitio de SharePoint mediante JavaScript)
- Personalizaciones de búsqueda de SharePoint
- Receta de complemento de SharePoint: tipo de campo (usando la representación del lado cliente