Compartilhar via


Padrões e desempenho do JavaScript

Anos atrás, ASP.NET nos deu a renderização do controle de interface do usuário do lado do servidor, e foi bom. Essa renderização do lado do servidor, no entanto, requer código de confiança total. Agora que fizemos a transição para o SharePoint e Office 365, o código de confiança total não é mais uma opção. Isso significa que a renderização de controle de interface do usuário do lado do servidor não funcionará mais para nós.

No entanto, as empresas ainda precisam de funcionalidade de interface do usuário personalizada para seus sites e aplicativos. Isso significa que a funcionalidade personalizada da interface do usuário deve ser movida do lado do servidor para o lado do cliente.

O JavaScript do lado do cliente agora é o caminho a percorrer para a renderização do controle de interface do usuário.

Padrões JavaScript

Como o JavaScript do lado do cliente é o caminho, quais são as melhores maneiras de implementar o JavaScript do lado do cliente? Como se começa?

Há várias opções:

Opção Descrição
JavaScript Embedding Site.UserCustomActions ou Web.UserCustomActions permitem a inclusão do script diretamente na marcação da página. Isso é usado no Padrão do Carregador discutido abaixo
Modelos de exibição Aplica-se a Exibições e Pesquisa. Você não precisa implantar nenhum tipo de código hospedado por aplicativo ou provedor. É simplesmente um arquivo JavaScript que pode ser carregado para (por exemplo) a biblioteca de estilos para personalizar exibições. Você pode criar qualquer exibição necessária usando JavaScript
Add-Ins hospedado do SharePoint Usa o JSOM para se comunicar novamente com a Web do host ou a Web de suplemento. Ele dá acesso ao Proxy Web para chamadas de domínio cruzado
Provedor hospedado Add-Ins Permite a criação de aplicativos complexos em uma variedade de pilhas de tecnologia – mantendo a integração segura com o SharePoint
JSLink Permite carregar um ou mais arquivos JavaScript em muitas web parts e exibições do OOTB
ScriptEditor Webpart Incluir script diretamente ou carregado por meio de marcas de script com marcação para criar aplicativos de página única complexos hospedados inteiramente no site do SharePoint

Não pense que você está preso a essas opções se achar que uma opção diferente seria melhor para sua situação.

Desempenho do JavaScript

Em cada etapa do processo de desenvolvimento, é importante ter o desempenho em mente. Aqui estão algumas coisas que fazem uma grande diferença no desempenho do JavaScript:

Opção Descrição
Reduzir o número de solicitações Menos solicitações significa menos ida e volta para o servidor, reduzindo a latência.
Recuperar apenas os dados de que você precisa Reduza a quantidade de dados enviados pelo fio. Também reduz a carga do servidor.
Fornecer uma boa experiência de carregamento de página Mantenha a interface do usuário responsiva ao usuário. Por exemplo, atualize os menus na página antes de iniciar o download de mais de 100 registros.
Usar chamadas e padrões assíncronos sempre que possível A votação é um fardo mais pesado sobre o desempenho do que usar uma chamada assíncrona ou retorno de chamada.
O cache é a chave O cache reduz ainda mais a carga sobre o servidor, ao mesmo tempo em que proporciona melhoria imediata de desempenho.
Preparar-se para mais exibições de página do que você jamais imaginou Uma página de destino pesada de dados é boa quando você tem apenas alguns acessos. Mas se você receber milhares de acessos, isso pode realmente afetar o desempenho.

O que meu código está fazendo

Para o desempenho, é importante saber o que seu código está fazendo a qualquer momento. Isso permite identificar maneiras de melhorar a eficiência. Abaixo estão algumas boas maneiras de fazer exatamente isso.

Reduzir o número de solicitações

Sempre faça as menores e menores solicitações possíveis. Cada solicitação que você elimina reduz a carga de desempenho no servidor e no cliente. E fazer solicitações menores reduz ainda mais a carga de desempenho.

Há várias maneiras de reduzir as solicitações e reduzir o tamanho da solicitação.

  • Limite o número de arquivos JavaScript em produção. Separar seus arquivos JavaScript funciona bem para o desenvolvimento, mas não tão bem para produção. Combine seus arquivos JavaScript em um único arquivo JavaScript ou o menor número possível de arquivos JavaScript.
  • Reduzir tamanhos de arquivo. Minimize seus arquivos JavaScript de produção removendo quebras de linha, espaço em branco e comentários. Há vários programas e sites javaScript que você pode usar para reduzir consideravelmente seus tamanhos de arquivo JavaScript.
  • Use o cache de arquivo do navegador para reduzir as solicitações. O Padrão do Carregador atualizado abaixo é uma boa maneira de expandir essa ideia.

Recuperar apenas os dados de que você precisa

Ao solicitar dados, lembre-se de concentrar suas solicitações no que você realmente precisa. Baixar um artigo inteiro para obter o título, por exemplo, reduzirá bastante o desempenho.

  • Use filtragem de servidor, seleção e limites para minimizar o tráfego sobre o fio.
  • Não solicite todos os artigos quando desejar apenas os cinco primeiros, como outro exemplo.
  • Não peça o saco inteiro da propriedade se você quiser apenas uma propriedade. Em um exemplo, um script precisava apenas de uma propriedade, mas solicitou todo o saco de propriedades, que acabou sendo de 800 KB. Lembre-se também de que o tamanho de um objeto pode mudar ao longo do tempo, portanto, o que é apenas alguns quilobytes agora pode se tornar megabytes em tamanho mais tarde no ciclo de vida do produto.

Não solicite dados que você descartará não utilizados

Se você recuperar mais dados do que realmente usa, pense nela como uma oportunidade para filtrar melhor sua consulta inicial.

  • Solicite apenas os campos necessários, como Nome e Endereço, e não todo o registro.
  • Faça solicitações de filtro específicas e deliberadas. Por exemplo, se você quiser listar os artigos disponíveis, obtenha o Título, o PublishingDate e o Autor. Deixe o restante dos campos fora da solicitação.

Fornecer uma boa experiência de usuário

Interfaces de usuário irregulares e inconsistentes afetam não apenas o desempenho, mas também o desempenho percebido. Escreva seu código de forma a dar uma experiência suave.

  • Use um rotador para indicar que as coisas estão carregando ou levando tempo.
  • Entenda a ordem de execução do código e modele-a para a melhor experiência do usuário. Por exemplo, se você planeja recuperar muitos dados do servidor e planeja alterar a interface do usuário ocultando um menu, esconda o menu primeiro. Isso impedirá uma experiência de interface do usuário escalonada para o usuário.

Tudo é assíncrono

Todas as atividades de código no navegador devem ser consideradas assíncronas. Seus arquivos são carregados em alguma ordem, você deve aguardar o carregamento do DOM e suas solicitações de volta ao SharePoint serão concluídas em velocidades diferentes.

  • Entenda como seu código opera a tempo.
  • Use eventos e retornos de chamada em vez de sondagem.
  • Use promessas. No jQuery, eles são chamados de objetos adiados . Há conceitos semelhantes em Q, WinJS e ES6.
  • Use a chamada assíncrona em favor da chamada não assíncrona.
  • Use assíncrono sempre que houver um atraso:
    • Durante uma solicitação do AJAX.
    • Durante qualquer manipulação significativa do DOM.

Padrões assíncronos melhoram o desempenho e a capacidade de resposta e permitem o encadeamento efetivo de ações dependentes.

Cache do Lado do Cliente

O cache do lado do cliente é um dos aprimoramentos de desempenho mais frequentemente perdidos que você pode adicionar ao seu código.

Há três locais diferentes que você pode armazenar em cache seus dados:

Opção Descrição
Armazenamento de Sessão Armazena dados como um par de chave/valor no cliente. Isso é por armazenamento de sessão que é sempre armazenado como cadeias de caracteres.

JSON.stringify() converterá seus objetos JavaScript em cadeias de caracteres que ajudam a armazenar objetos.
Armazenamento local Armazena dados como um par de chave/valor no cliente. Isso é persistente entre as sessões que são sempre armazenadas como cadeias de caracteres.

JSON.stringify() converterá seus objetos JavaScript em cadeias de caracteres que ajudam a armazenar objetos.
Banco de Dados Local Armazena dados relacionais no cliente. Usa com frequência SQL-Lite como o mecanismo de banco de dados.

O armazenamento de banco de dados local nem sempre está disponível em todos os navegadores — Verifique o suporte ao navegador de destino

Ao fazer cache, tenha em mente os limites de armazenamento disponíveis para você e o frescor de seus dados.

  • Se você estiver atingindo o fim dos limites de armazenamento, talvez seja sábio remover os dados armazenados em cache mais antigos ou menos importantes.
  • Diferentes tipos de dados podem ficar obsoletos mais rapidamente do que outros. Uma lista de artigos de notícias pode ficar obsoleta em cinco a dez minutos, mas o nome do perfil de um usuário pode ser armazenado em cache com segurança por 24 horas ou mais.

O armazenamento local e de sessão não tem expiração interna, mas os cookies têm. Você pode vincular seus dados armazenados a um cookie para adicionar expiração ao armazenamento local e de sessão. Você também pode criar um wrapper de armazenamento que inclua uma data de validade e marcar isso em seu código.

O preço da popularidade

Com que frequência sua página é exibida? No cenário clássico, a home page corporativa é definida como a página de inicialização para todos os navegadores em toda a organização. Então de repente você tem muito mais tráfego do que você jamais imaginou. Cada byte de conteúdo é subitamente ampliado no desempenho do servidor e na largura de banda que sua home page consome.

A solução: acesse na home page e vincule-se ao outro conteúdo.

Painéis pesados de dados também são candidatos a um aplicativo hospedado pelo provedor que pode ser dimensionado de forma independente.

O padrão do carregador

O objetivo do padrão do carregador é fornecer uma maneira de inserir um número desconhecido de scripts remotos em um site sem precisar atualizar o site. As atualizações podem ser feitas na CDN remota e atualizarão todos os sites.

O Padrão do Carregador constrói uma URL com carimbo de data e hora no final para que o arquivo não seja armazenado em cache. Ele configura jQuery como uma dependência no arquivo carregador e, em seguida, executa uma função no carregador. Isso garante que o JavaScript personalizado seja carregado após o jQuery concluir o carregamento.

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();
    });
}

O SP.SOD.registerSodDep('pnp-loader.js', 'pnp-jquery.js'); configura a dependência e SP.SOD.executeFunc('pnp-loader.js', null, function() {}); força jQuery a carregar completamente antes de carregar o JavaScript personalizado.

O newLink.ScriptBlock = script2; e newLink.Location = "ScriptLink"; são as principais partes de adicionar isso à ação do cliente do usuário.

O arquivo pnp-loader.js carrega uma lista de arquivos JavaScript, com uma promessa que pode ser executada quando cada arquivo é carregado.

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 */
});

O arquivo pnp-loader.js não armazena em cache, o que funciona bem para um ambiente de desenvolvimento. O arquivo pnp-loader-cached.js substitui a $.getScript função por uma $.ajax função que permite o cache do navegador dos arquivos e é mais adequado para produção.

De 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);

Esse padrão facilita a implantação e as atualizações para sites. Ele é especialmente útil ao implantar ou atualizar em milhares de coleções de sites.

Cache do usuário atual

Se as informações do usuário já estiverem armazenadas em cache, essa função obterá os dados do cache da sessão. Se as informações do usuário não forem armazenadas no cache, elas receberão as informações específicas de usuário necessárias e as armazenarão no cache.

Isso também usa Deferred (versão jQuery da promessa). Se recebermos os dados do cache ou do servidor, o adiado será resolvido. Se houver um erro, o adiado será rejeitado.

De 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();
    }
}

Padrão de cache usando assíncrono e adiado

Outro padrão de cache pode ser encontrado em pnp-clientcache.js storageTest, que é retirado do armazenamento do modernizadorTest. Ele contém funções para adicionar, obter, remover e getOrAdd que retornarão os dados armazenados em cache se estiverem no cache ou recuperarão os dados do servidor e os adicionarão ao cache se ele não estiver no cache que salva a gravação de código repetitivo na função de chamada. get usa JSON.parse para testar a expiração, pois a expiração não é um recurso no armazenamento local. _createPersistable armazena o objeto JavaScript no cache de armazenamento local.

De 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 obter um uso mais complexo de assíncrono e adiado, você pode consultar o dashboard do desenvolvedor no pnp-clientcache.js

Recursos

Confira também

Exemplos