Рубрики
Без рубрики

Решение проблемы Lighthouse “Избегайте чрезмерного размера DOM”

Недавно мы начали немного изучать производительность OSBO. Поскольку страница была создана в основном в то время, когда… Помеченный как vue, next, производительность, java.

Недавно мы начали немного изучать производительность OSBO . Поскольку страница была создана в основном в то время, когда мы не очень хорошо разбирались в интерфейсной разработке (по-британски: мы понятия не имели, что делаем ), плюс у нас не было активного мониторинга производительности, очевидно, удалось проникнуть различным проблемам.

Существует множество статей о том, как запустить Lighthouse, и они содержат ряд очень полезных предложений, поэтому я не буду повторять это здесь. Была одна проблема, в которой совет был не особенно дружелюбным: “Избегайте чрезмерного размера DOM”. В нашем случае даже на наших домашних страницах и страницах регистрации было около 3500 узлов DOM, и, учитывая, что они довольно просты, это звучало чрезмерно. Мы изо всех сил пытались понять, откуда берутся все эти узлы. Все советы касались “избегайте создания слишком большого количества узлов DOM”, но я просто не смог найти никакой полезной информации о том, как мне узнать, где (логически в моей кодовой базе) создаются узлы. В какой части моего кода проблема? Трудно оптимизировать, пока вы не знаете, какие компоненты вам нужно оптимизировать.

Итак, я быстро разработал инструмент, который поможет нам найти “узкие места ДОМЕНА”. И поскольку я все еще люблю Java (точнее: это инструмент, в котором я наиболее продуктивен), он на Java – извините, ребята 😉

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

Чтобы сделать это, не тратя слишком много времени на написание самого инструмента (в конечном итоге мне потребовалось около 30 минут) Я решил использовать JSoup (для анализа дерева DOM с нашего веб-сайта), а Джексон – для красивой печати результатов, так как затем я могу легко свернуть/развернуть JSON в IntelliJ (полезный совет: откройте любой файл .json и нажмите CTRL-ALT-L , чтобы красиво отступить от одной массивной строки JSON).

Полный результат находится в Репозиторий Github (У меня такое чувство, что нам может понадобиться больше материала, чтобы помочь нам с отчетами о маяках). В проекте есть два класса:

OsboDomNode – класс, представляющий DOM с точки зрения того, что нас волнует: общее количество дочерних (и внучатых… дочерних узлов) и некоторая базовая статистика по прямым дочерним узлам. Он использует рекурсивные функции для агрегирования общего количества узлов в каждом из элементов DOM.

package online.onestopbeauty.blog.examples.lighthouse.dom;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.*;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static java.util.Collections.emptyList;
import static java.util.Comparator.naturalOrder;
import static java.util.stream.Collectors.groupingBy;

@Data
@Builder
@JsonPropertyOrder({ "description", "type", "allChildNodesCount", "childNodesSummary" })
public class OsboDomNode {

    private final String type;
    private final String description;
    @JsonIgnore
    @Singular
    private final List childNodes;

    @Getter(AccessLevel.NONE)
    private Integer allChildNodesCount;

    public int getAllChildNodesCount() {
        if (allChildNodesCount == null) {
            allChildNodesCount = this.childNodes.size() + this.childNodes.stream().mapToInt(OsboDomNode::getAllChildNodesCount).sum();
        }
        return allChildNodesCount;
    }

    public List getChildNodesSummary() {
        Integer allChildNodesCount = this.getAllChildNodesCount();
        return this.childNodes.stream().map(child -> percentageInChild(child, allChildNodesCount)).collect(Collectors.toList());
    }

    public List getNodesWithHighestNumberOfChildren() {
        Map> nodesWithChildCount = childNodes.stream().collect(groupingBy(OsboDomNode::getAllChildNodesCount));
        Optional maxNodes = nodesWithChildCount.keySet().stream().max(naturalOrder());
        if (maxNodes.isPresent()) {
            return nodesWithChildCount.get(maxNodes.get());
        } else {
            return emptyList();
        }
    }

    private String percentageInChild(OsboDomNode child, Integer allChildNodesCount) {
        double percentage = 100.0 * child.getAllChildNodesCount() / allChildNodesCount;
        return String.format("%d [%.2f%%] in %s %s", child.getAllChildNodesCount(), percentage, child.type, child.description);
    }

    public static OsboDomNode fromElement(Element element) {
        OsboDomNode.OsboDomNodeBuilder builder = OsboDomNode.builder();
        builder.type(element.tag().getName() + "[" + element.siblingIndex() + "]");
        builder.description(element.attributes().toString());

        Elements children = element.children();
        children.forEach(child -> builder.childNode(OsboDomNode.fromElement(child)));
        return builder.build();
    }
}

OsboPerfHelper – простой бегун, вы вводите URL-адрес своего веб-сайта (может быть даже локальный хост) , он отключается, считывает структуру DOM, а затем мы передаем ее в OsboDomNode для анализа.

package online.onestopbeauty.blog.examples.lighthouse.dom;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import java.io.File;
import java.io.IOException;

public class OsboPerfHelper {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    public static void main(String[] args) throws IOException {
        String osboUrl = "http://localhost:8081";
        Document doc = Jsoup.connect(osboUrl).get();
        Element body = doc.body();
        OsboDomNode osboDomNode = OsboDomNode.fromElement(body);
        System.out.println((Integer) osboDomNode.getAllChildNodesCount());
        printJson(osboDomNode);

    }

    private static void printJson(OsboDomNode osboDomNode) throws IOException {
// System.out.println(OBJECT_MAPPER.writeValueAsString(osboDomNode));
        File resultFile = new File("domNode.json");
        OBJECT_MAPPER.writeValue(resultFile, osboDomNode);
        System.out.println("Written JSON result into " + resultFile.getAbsolutePath());
    }

}

Соответствующий файл build.gradle

plugins {
    id 'java'
}

group 'online.onestopbeauty.blog.examples'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
// https://mvnrepository.com/artifact/org.jsoup/jsoup
    compile group: 'org.jsoup', name: 'jsoup', version: '1.12.1'
// https://mvnrepository.com/artifact/org.projectlombok/lombok
    compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.8'
// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.9'

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

О да, я использую Ломбок для конструкторов, строителей и других шаблонов (добытчиков и т. Д.) – просто потому, что Ломбок потрясающий и это первое, что я всегда добавляю в любой Java-проект. Просто не забудьте добавить плагин Lombok и включить обработку аннотаций в IntelliJ, иначе вы получите ошибки компиляции.

Итак, как все выглядело для нас, когда мы работали в живой версии? Первые несколько уровней узлов выглядели довольно здоровыми, с основными и прямыми подузлами, содержащими около 99% узлов каждый (просто несколько слоев обертки, не о чем беспокоиться). Но потом я увидел что-то подозрительное (и вот подсказка для проверки использования значимых имен классов в компонентах – значительно упрощает устранение неполадок):

{
"description": " class=\"application--wrap\"",
"type": "div[0]",
"allChildNodesCount": 3401,
"childNodesSummary": [
  "[39.05] in div[2] class=\"layout\" data-v-3a808de6",
  "[56.40] in main[4] class=\"v-content\" style=\"padding-top:0px;padding-right:0px;padding-bottom:56px;padding-left:0px;\"",
  "[4.38] in footer[6] data-cy=\"osboFooter\" class=\"v-footer v-footer--absolute v-footer--inset theme--light\" style=\"height:auto;margin-bottom:56px;border-radius:10px;\" data-v-3645c51c",
  "[0.06] in button[8] type=\"button\" medium=\"\" class=\"v-btn v-btn--bottom v-btn--floating v-btn--fixed v-btn--right v-btn--small theme--dark secondary fab-style\" style=\"display:none;\" data-v-045da490"
]}

“Основная” часть нашего приложения занимала менее 60% узлов, а элемент “div[2]/макет” занимал почти 40%. На этом этапе я добавил дополнительную инструкцию журнала в OsboPerfHelper, перейдя к правильному узлу. Это, конечно, можно было бы сделать гораздо более приятным способом, и если бы мне пришлось использовать его чаще, возможно, я бы добавил более приятный инструмент “детализации” – но на данный момент это была “быстрая и грязная” работа на полчаса или около того – и сделал работу достаточно хорошо:

printJson(osboDomNode.getNodesWithHighestNumberOfChildren().get(0)
                .getNodesWithHighestNumberOfChildren().get(0)
                .getNodesWithHighestNumberOfChildren().get(0)
                .getNodesWithHighestNumberOfChildren().get(0)
                .getChildNodes().get(0));

Результатом было:

{
"description": " class=\"flex offset-md1 md10 xs12\" data-v-3a808de6",
"type": "div[0]",
"allChildNodesCount": 1327,
"childNodesSummary": [
   "[0.45] in div[0] class=\"layout\" data-v-0c4978b8 data-v-3a808de6",
   "[65.49] in aside[2] data-cy=\"mobileNavBar\" class=\"offWhite1 v-navigation-drawer v-navigation-drawer--clipped v-navigation-drawer--close v-navigation-drawer--fixed v-navigation-drawer--temporary theme--light\" style=\"height:100%;margin-top:0px;transform:translateX(-375px);width:375px;\" data-v-c332d172 data-v-3a808de6",
    "[33.84] in nav[4] id=\"attachMenu\" data-cy=\"osboToolBar\" class=\" text-xs-center px-0 toolbarStyle v-toolbar elevation-0 v-toolbar--dense v-toolbar--extended theme--light\" style=\"margin-top:0px;padding-right:0px;padding-left:0px;transform:translateY(0px);\" data-v-3a808de6"
]}

Это позволило мне увидеть, что у меня почти 900 узлов в моем мобильном намбаре. Самое смешное было то, что мне даже не нужна мобильная панель навигации (с мобильной версией меню) на настольной версии страницы, которую я тестировал на данный момент. Таким образом, мы пошли и сделали несколько простых очисток, чтобы уменьшить размер мобильного меню (900 узлов звучит чрезмерно, даже когда это необходимо) и убедиться, что оно не создается на настольных компьютерах (так как это пустая трата и никогда не отображается).

Это было только начало обрезки дерева DOM (сейчас у нас около 1700 узлов на локальном хосте, поэтому значительное сокращение и еще больше впереди), но главное было знать, какие “ветви” DOM обрезать.

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

Оригинал: “https://dev.to/lilianaziolek/solving-lighthouse-avoid-an-excessive-dom-size-issue-4b7b”