Wszystkie artykuły
Osadzenie10 min czytania

Osadzanie widgetu wsparcia w aplikacjach single-page: zmiana trasy, Shadow DOM, czas ładowania

Co widget wsparcia musi umieć w aplikacji single-page: przetrwać zmiany trasy, izolować style, nie spowalniać strony. Z technicznymi powodami stojącymi za tym.

Martin Semmele

Uporządkowane biurko z dużym monitorem pokazującym tylko miękkie, rozmyte plamy koloru, przed nim klawiatura i zamknięty notes, za nim ceglana ściana i okno
Widget supportu to obcy skrypt na stronie, którą zbudował ktoś inny. Osadzenie decyduje, czy oboje się dogadają. · Wygenerowano przez AI

Najważniejsze wnioski

  • 92 procent wszystkich stron internetowych wczytuje co najmniej jeden zasób zewnętrzny; widget wsparcia jest jednym z nich i musi się odpowiednio zachowywać.
  • Widget zamontowany raz przy dokumencie, a nie żyjący w drzewie aplikacji, przeżywa każdą zmianę trasy po stronie klienta bez twojego udziału.
  • Shadow DOM rozdziela style w obie strony, ale nie fokus klawiatury, czcionki ani kolejność stosu względem banera cookie.
  • Skrypt z atrybutem async nie blokuje parsowania strony; to ta jedna linijka decyduje o czasie ładowania.
  • Podwójny montaż, utracone identyfikatory rozmowy i dociągany pakiet czcionek to trzy błędy, które w praktyce występują najczęściej.

Dlaczego obcy skrypt w aplikacji single-page żyje inaczej

Na klasycznej stronie internetowej każdy klik powoduje wczytanie nowej strony. Skrypt umieszczony na końcu dokumentu uruchamia się więc przy każdej odsłonie od nowa, a wszystko, co chce zapamiętać, musi gdzieś zapisać. W aplikacji single-page jest odwrotnie: dokument wczytuje się dokładnie raz, a potem framework wymienia jedynie fragmenty drzewa. Skrypt, który raz się wykonał, nie uruchamia się ponownie.

Dla widgetu wsparcia to na początek dobra wiadomość. Nie musi budować się od nowa przy każdej zmianie trasy, a otwarta rozmowa po prostu pozostaje otwarta. Zła wiadomość wynika z tego samego faktu: wszystko, co widget robi źle, również pozostaje aktualne, dopóki ktoś twardo nie odświeży strony. Konflikt stylów, drugi launcher w prawym dolnym rogu, fokus zawieszony w widgecie - żadna zmiana trasy tego nie posprząta.

Według Web Almanac 2024 aż 92 procent zbadanych stron wczytuje co najmniej jeden zasób zewnętrzny, a wśród tysiąca największych stron mediana wynosi 66 różnych dostawców zewnętrznych1. Twój widget wsparcia jest jednym z nich. Konkuruje o czas ładowania, kolejność stosu i fokus klawiatury ze skryptami analitycznymi, banerami cookie i pakietami czcionek. Trzy kolejne sekcje rozbierają te trzy konflikty pojedynczo.

Zmiana trasy: widget nie należy do drzewa aplikacji

Najczęstszy błąd przy wdrażaniu w React, Vue czy Angular jest dobrze intencjonowany: widget trafia do aplikacji jako komponent, żeby „porządnie” stać w drzewie. Przez to jego życie zależy od cyklu życia tego komponentu. Gdy trasa się zmienia i layout renderuje się na nowo, widget jest demontowany i montowany ponownie. Widoczny efekt: okno czatu się zwija, historia znika albo pojawia się podwójnie, a w zależności od implementacji otwiera się druga rozmowa.

Zamontuj raz w dokumencie, nie w komponencie.

Najbardziej odporna wersja jest zarazem najprostsza: skrypt wczytuje się raz, podpina swój kontener bezpośrednio do elementu body i żyje poza wszystkim, czym zarządza framework. Zmiana trasy wymienia węzły wewnątrz kontenera aplikacji; leżący obok kontener widgetu pozostaje nietknięty. Dokładnie tak działa widget Comlayer: odczytuje własny tag script przez document.currentScript, tworzy pojedynczy element hosta przy body i przed montażem sprawdza, czy ten element już istnieje. Gdy skrypt wykona się drugi raz, na przykład dlatego, że framework powtarza kod wdrażania przy rerenderze, nic się nie dzieje.

Dwa niuanse są tu ważne. Po pierwsze: document.currentScript nic nie zwraca, gdy kod działa z poziomu funkcji zwrotnej, zdarzenia lub modułu JavaScript2. Widget, który na tym polega, potrzebuje rozwiązania awaryjnego, na przykład wyszukania ostatniego tagu script z pasującym atrybutem danych. Po drugie: adres strony przypisany do rozmowy to adres z chwili jej otwarcia. Gdy osoba odwiedzająca zmieni potem trasę pięć razy, w skrzynce pozostaje adres początkowy. To nie błąd, tylko decyzja; kto potrzebuje aktualnego adresu, musi o niego zapytać w samej wiadomości.

Gdzie leży historia rozmowy.

Żeby rozmowa przeżyła twarde odświeżenie, widget musi zapamiętać identyfikator osoby odwiedzającej i identyfikator rozmowy. Zwyczajowe miejsce to localStorage przeglądarki, zapisany pod prefiksem zawierającym klucz widgetu. Dwa widgety na tej samej domenie, na przykład na stronie marketingowej i w aplikacji za nią, nie wchodzą sobie wtedy w drogę. To, co tam nie należy, to sama treść rozmowy: leży ona u dostawcy, a widget dociąga ją na podstawie identyfikatora. Gdy zapis zniknie, bo osoba odwiedzająca go usunie albo przegląda stronę w trybie prywatnym, przy następnej wizycie zaczyna się nowa rozmowa, a stara pozostaje zachowana w skrzynce zespołu.

Miejsce wdrożeniaZachowanie przy zmianie trasyZachowanie przy twardym odświeżeniu
Jako komponent w drzewie aplikacjiZależnie od layoutu demontowany i montowany ponownie, historia skacze lub się duplikujeMontowany ponownie, historia dociągnięta z pamięci
Jako skrypt w dokumencie, zamontowany razNietknięty, rozmowa pozostaje otwartaMontowany ponownie, historia dociągnięta z pamięci
Wstawiony przez menedżer tagówJak w dokumencie, o ile tag wystrzeli tylko razJak w dokumencie

Shadow DOM: co daje rozdzielenie stylów, a czego nie

Widget przynosi własne style, a twoja strona ma swoje. Bez rozdzielenia globalna reguła button { border-radius: 0 } w twoim arkuszu stylów trafia też w launcher widgetu, a odwrotnie zbyt szeroko sformułowany selektor widgetu pokoloruje twoje formularze. Standardowa droga, by temu zapobiec, to Shadow DOM: widget renderuje się do własnego poddrzewa, którego style nie są osiągalne z zewnątrz i które samo nie oddziałuje na zewnątrz. Dokumentacja Mozilli podsumowuje to tak: CSS strony nie działa na węzły w Shadow DOM, a style w Shadow DOM nie działają na resztę strony3.

Cztery rzeczy, których granica nie zatrzyma.

Rozdzielenie stylów jest pełne, ale to tylko rozdzielenie stylów. Cztery rzeczy i tak przechodzą przez granicę, i wszystkie cztery pojawiają się w zgłoszeniach kierowanych do dostawców widgetów:

  • Właściwości dziedziczone. Krój pisma, rozmiar czcionki i kolor tekstu są dziedziczone z elementu hosta, jeśli widget sam ich nie ustawi. Widget, który nie ustawia wyraźnie swojej czcionki, wygląda na każdej stronie inaczej.
  • Fokus klawiatury. Klawisz Tab przechodzi przez całą stronę, Shadow DOM czy nie. Otwarte okno czatu, które samo nie trzyma fokusu, pozwala klawiaturze za oknem wędrować po nawigacji strony.
  • Kolejność stosu. Baner cookie z z-index: 99999 przykryje launcher, jeśli widget wybierze mniejszą wartość. Dlatego widgety ustawiają swój host na największą możliwą wartość i izolują go przez isolation: isolate, żeby ta wartość skrajna nie oddziaływała na konteksty stosu strony.
  • Język i kierunek pisania. lang i dir są dziedziczone z elementu hosta. Strona w języku hebrajskim lub arabskim przełącza tym samym widget na pisanie od prawej do lewej, niezależnie od tego, czy on to potrafi.

Shadow DOM w trybie open nie jest przy tym mechanizmem bezpieczeństwa: JavaScript strony może nadal wnikać do środka przez shadowRoot. Tryb reguluje jedynie dostęp skryptowy, a nie rozdzielenie stylów, i to obowiązuje w obu trybach3.

Czas ładowania: decyduje jedna linijka kodu

Obcy skrypt kosztuje czas ładowania. Pytanie tylko, czy kosztuje ten czas w tle, czy na pierwszym planie, gdy osoba odwiedzająca patrzy na pustą stronę. Różnica to jeden atrybut: tag script bez async lub defer wstrzymuje parsowanie dokumentu, dopóki skrypt nie zostanie wczytany i wykonany. Zalecenie twórców Chrome jest więc jednoznaczne: obce skrypty ładować zawsze asynchronicznie, chyba że skrypt musi się wykonać, zanim strona może zostać wyrenderowana4. Widget wsparcia nigdy tego nie musi.

Async, nie defer, i dlaczego widget nie dociąga czcionek.

async wykonuje skrypt, gdy tylko jest wczytany, niezależnie od kolejności w dokumencie. defer czeka, aż dokument zostanie w całości sparsowany, i zachowuje kolejność4. Dla widgetu, który ze strony nie potrzebuje nic poza elementem body, async jest właściwym wyborem; może spokojnie być gotowy wcześniej niż reszta. Osadzenie Comlayer składa się dlatego z jednego tagu z dokładnie tym atrybutem, na przykład <script src="https://app.comlayer.app/comlayer-widget.js" data-app-id="wgt_…" async></script>. Interfejsu JavaScript do otwierania, zamykania czy zgłaszania zmiany trasy nie ma, i niczego nie brakuje: widget go nie potrzebuje, bo żyje poza aplikacją.

Druga pozycja na rachunku za czas ładowania jest często pomijana: czcionki. Widget, który dociąga swoją firmową czcionkę przez @import z serwisu czcionek, wywołuje na każdej stronie klienta kolejne zapytanie do kolejnego serwisu, dla każdej pojedynczej osoby odwiedzającej. To kosztuje czas i jest, w zależności od serwisu i jego siedziby, dodatkowo przekazaniem danych, które musiałoby się znaleźć w informacji o ochronie danych strony klienta. Comlayer usunął ten import z widgetu i wraca do czcionki systemowej; zbudowana paczka ma dzięki temu około 34 kilobajtów, przesyłanych w formie skompresowanej, zmierzone 17 września 2026.

WskaźnikPróg dla „dobrze”Co widget do tego dokłada
Largest Contentful Paint (LCP)2,5 sekundyTylko gdy skrypt ładuje się synchronicznie albo dociąga własne czcionki
INP (czas reakcji na interakcje)200 milisekundDługie wykonywanie skryptu w głównym wątku przy starcie
Cumulative Layout Shift (CLS)0,1Launcher, który po załadowaniu zajmuje miejsce i przesuwa treść

Trzy progi pochodzą z Core Web Vitals i obowiązują każdorazowo dla 75. percentyla odsłon strony, osobno dla urządzeń mobilnych i desktopu5. Widget, który ładuje się asynchronicznie, ma stałą pozycję launchera i nie dociąga czcionek, nie pojawia się w żadnej z trzech liczb.

Wdrożenie za pomocą menedżera tagów

Wiele zespołów osadza obce skrypty nie w kodzie źródłowym, lecz przez menedżer tagów. Dla widgetu wsparcia jest to w porządku, pod dwoma warunkami. Po pierwsze, tag musi wystrzelić dokładnie raz, przy ładowaniu dokumentu, a nie przy każdej wirtualnej odsłonie strony, którą aplikacja single-page zgłasza menedżerowi tagów. Tag, który nasłuchuje zdarzenia „odsłona strony”, wstawia skrypt ponownie przy każdej zmianie trasy. Widget, który sam wyłapuje podwójny montaż, wybacza to; taki, który tego nie robi, pokazuje potem dwa launchery.

Po drugie, skrypt wstawiony przez menedżer tagów w pewnych okolicznościach nie działa już jako ten tag script, którym byłby w kodzie źródłowym. Widget, który odczytuje swój klucz przez document.currentScript, nic wtedy nie znajduje i potrzebuje opisanego wyżej rozwiązania awaryjnego. Sprawdź po osadzeniu w konsoli, czy przy body wisi dokładnie jeden element hosta i czy widget znalazł swój klucz. Obie rzeczy widać w narzędziach deweloperskich w kilka sekund.

Zgoda i ładowanie po kliknięciu

W Unii Europejskiej przy osadzaniu pojawia się pytanie, które z techniką ma związek tylko na marginesie: czy skrypt może ładować się od razu, czy dopiero po zgodzie? Ocena prawna jest sporna i przedstawiliśmy ją szczegółowo w osobnym wpisie o widgecie wsparcia i zgodzie. Technicznie ostrożny wariant oznacza: tag script nie jest wpisywany statycznie do dokumentu, lecz generowany przez narzędzie do zarządzania zgodą, gdy tylko osoba odwiedzająca się zgodziła.

Dla aplikacji single-page nie zmienia to nic w zasadzie podstawowej. Także wygenerowany później tag script podpina swój element hosta raz do body i pozostaje tam przez każdą zmianę trasy. Zmienia się moment: widget pojawia się dopiero po kliknięciu, a rozmowa, która przed zgodą nie była możliwa, nie może też stracić historii. Kto wybiera ten wariant, powinien mimo to zarezerwować miejsce launchera, żeby strona nie skakała, gdy widget się pojawi.

Lista kontrolna po wdrożeniu

Czy osadzenie jest poprawne, da się sprawdzić w kilka minut. Poniższe punkty obejmują błędy, które w praktyce występują najczęściej:

  1. 01Otwórz rozmowę, potem zmień trasę trzy razy. Okno pozostaje otwarte, historia zostaje, nie powstaje druga rozmowa.
  2. 02Odśwież stronę twardo. Po załadowaniu historia jest znów na miejscu, a identyfikator rozmowy w pamięci jest taki sam jak wcześniej.
  3. 03Sprawdź w narzędziach deweloperskich, czy przy body wisi dokładnie jeden element hosta, także po kilku zmianach trasy i powrocie do trasy startowej.
  4. 04Otwórz baner cookie, gdy okno czatu jest otwarte. Oba muszą pozostać obsługiwalne, żadne nie może zasłaniać drugiego.
  5. 05Przejdź klawiszem Tab przez otwarte okno czatu. Fokus musi być widoczny i nie może znikać w stronie za oknem.
  6. 06Sprawdź w zakładce sieciowej, czy po skrypcie widgetu nie jest ładowany żaden plik czcionki z obcego serwisu.
  7. 07Zmierz stronę narzędziami Lighthouse, raz z widgetem i raz bez. Trzy Core Web Vitals nie mogą się pogorszyć.

Comlayer przechodzi tę listę, bo widget jest zbudowany dokładnie do tego: jeden tag script, jeden host przy body, Shadow DOM z własnymi stylami, brak obcych czcionek, brak interfejsu, który framework musiałby obsługiwać. Jak widget wygląda i jakie moduły niesie, ustawiasz w panelu, bez przebudowy strony; co robi z danymi osób odwiedzających, opisano na stronie o bezpieczeństwie i ochronie danych.

Najczęściej zadawane pytania

Czy muszę inicjalizować widget na nowo przy każdej zmianie trasy?

Nie. Widget zamontowany raz przy dokumencie, który nie żyje w drzewie aplikacji, pozostaje przez każdą zmianę trasy po stronie klienta. Inicjalizacja per trasa jest konieczna tylko wtedy, gdy widget został wbudowany w aplikację jako komponent, a właśnie tego należy unikać.

Co się dzieje, gdy skrypt zostanie wstawiony dwa razy?

To zależy od widgetu. Porządnie zbudowany widget sprawdza przed montażem, czy jego element hosta już istnieje, i za drugim razem nic nie robi. Widget bez tej kontroli pokazuje dwa launchery i w pewnych okolicznościach otwiera dwie rozmowy. Menedżery tagów, które strzelają przy każdej wirtualnej odsłonie strony, są najczęstszą przyczyną.

Czy Shadow DOM w pełni chroni mój arkusz stylów przed widgetem?

Dla selektorów tak: style strony nie docierają do Shadow DOM, a style widgetu nie docierają do strony. Właściwości dziedziczone, takie jak krój pisma i kolor tekstu, fokus klawiatury, kolejność stosu oraz atrybuty lang i dir mimo to przechodzą przez granicę.

Async czy defer dla skryptu widgetu?

Async. Widget nie potrzebuje ze strony nic poza elementem body i może działać, gdy tylko jest wczytany. defer nie byłby błędem, ale niepotrzebnie czeka na koniec parsowania. Ważne jest tylko, żeby ustawiony był jeden z dwóch atrybutów; bez obu skrypt blokuje renderowanie.

Jak rozpoznać, czy widget pogarsza czas ładowania?

Zmierz stronę narzędziami Lighthouse raz ze skryptem i raz bez niego i porównaj LCP, INP i CLS. Widget ładowany asynchronicznie, ze stałym launcherem i bez dociąganych czcionek, nie zmienia tych trzech wartości w mierzalny sposób.

Czy mogę ładować widget dopiero po zgodzie?

Tak. Tag script nie jest wtedy wpisywany statycznie do dokumentu, lecz generowany przez narzędzie do zarządzania zgodą, gdy tylko osoba odwiedzająca się zgodziła. Także tak wygenerowany tag montuje widget raz przy dokumencie, a potem przeżywa on każdą zmianę trasy jak osadzony statycznie.

Źródła

  1. 01almanac.httparchive.org
  2. 02developer.mozilla.org
  3. 03developer.mozilla.org
  4. 04web.dev
  5. 05web.dev

Zacznij za darmo · Bez karty kredytowej

Skonfigurowane dziś wieczorem. Odpowiada już jutro rano.

Osadź widget, dodaj swoją wiedzę, gotowe — ComLayer przejmuje obsługę, także wtedy, gdy nikogo nie ma przy komputerze.