Дата: Воскресенье, Вчера, 19:40 | Сообщение # 1 |
|
Написал: Узнаваемый
Автор темы
Мурчанн
не в сети
Сообщений: 211
С самого начала меня не устраивала производительность старого скрипта. Он работал слишком медленно, фотографии отображались далеко не мгновенно, а счётчик вообще вёл себя нелогично: сначала показывал 0, и лишь после завершения полного подсчёта наконец-то реальное количество. Такая логика была очень далека от той плавной и понятной механики, которую мы привыкли видеть ВКонтакте. С первых же дней я стремился максимально приблизиться именно к этому эталону поведения сделать всё так же удобно, быстро и предсказуемо. Но, как это часто бывает, когда идеал недостижим в полной мере, приходится искать компромисс: если не получается реализовать задуманное на 100%, делаешь на максимум из того, что реально возможно в текущих условиях. И вот наконец наступил тот самый день я нашёл способ всё кардинально переделать. Появился свой собственный, правильный путь, который позволяет реализовать именно то, о чём я задумывал с самого начала. Разница между старым и новым (скриптами) довольно серьёзная. По сути новый скрипт , это оптимизированная версия старого, где убрал лишние механизмы и сделал мгновенный вывод миниатюр и счётчика. Разберу по-человечески и по пунктам. Главная идея нового скрипта Новый скрипт работает по принципу: 1. Сначала мгновенно показывает кэш из localStorage 2. Параллельно тихо загружает свежие данные 3. Обновляет кэш, но интерфейс почти не дёргаетПоэтому фотографии и счётчик появляются сразу, без ожидания загрузки. Как работал старый скрипт Старый скрипт был более «осторожным». Алгоритм примерно такой: 1. показать кэш 2. загрузить профиль 3. проверить изменился ли счётчик фото 4. если изменился загрузить альбом 5. только потом перерисовать DOMТо есть было больше проверок и блокировок. Главное отличие - загрузка изображений Старый скрипт Загрузка была через предзагрузчик Image() Код
const img = $('<img alt="Фото пользователя">').css({opacity:0}); const loader = new Image(); loader.onload = ()=>{ img.attr('src', item.src); requestAnimationFrame(()=>img.css('opacity',1)); }; loader.src = item.src;
Что происходит: 1. создаётся скрытый 2. создаётся new Image() 3. картинка загружается в память 4. после загрузки показываетсяМинусы 1. картинка появляется не сразу 2. DOM ждёт загрузки 3. лишняя память 4. больше кодаНовый скрипт Теперь всё проще: Код
const img = $('<img loading="lazy" alt="Фото пользователя">') .attr("src", p.src) .css({ opacity: 1, display: "block" });
Что происходит: сразу получает src браузер сам загружает используется loading="lazy"Плюсы 1. мгновенное появление 2. браузер сам оптимизирует 3. код проще 3. меньше CPUБлокировка рендера Старый скрипт Есть защита:Код
let renderLock = false;
и проверка: Код
if (renderLock) return;
Это делалось чтобы DOM не перерисовывался несколько раз. Новый скрипт Этого механизма вообще нет. Почему? Потому что: 1. сначала выводится кэш 2. потом обновляется 3. перерисовка безопасна 4. Код стал проще.Работа со счётчиком фото Старый скрипт Он проверял: Код
const cachedCount = parseInt(localStorage.getItem(LS_KEY_COUNT)) || 0; if(cachedCount === realCount) return;
Если число фотографий не изменилось, альбом вообще не загружался. Плюс экономия запросов.Минус иногда кэш мог устаревать.Новый скрипт Альбом загружается каждый раз: Но UI уже показан из кэша. Поэтому: 1. пользователь видит фотографии сразу 2. кэш обновляется тихоМгновенный показ счётчика Новый скрипт отдельно достаёт число: Код
const cachedCount = parseInt(localStorage.getItem(LS_COUNT)); counter.text(!isNaN(cachedCount) ? cachedCount : count);
Поэтому число фотографий появляется мгновенно. В старом скрипте оно часто появлялось после загрузки профиля.Упрощение кода Новый скрипт: 1. меньше функций 2. меньше условий 3. меньше блокировок 4. меньше манипуляций DOMНапример: старый Код
showCacheOrEmpty() loadProfileAndPhotos() renderPhotos() makeCounterClickable() normalizeUrl()
новый Код
showCache() load() render() makeCounterLink() normalize()
Логика стала прямее. Убрал cache-buster В старом скрипте: Код
$.get(profileUrl+'?_='+Date.now())
Это делалось чтобы обойти кэш браузера. В новом это убрал.Причина данные всё равно обновляются через localStorage. Lazy loading Новый скрипт использует: Это значит: 1. браузер не грузит картинки сразу 2. только когда они появляются на экранеЭто ускоряет страницу. Итог Новый скрипт делает три ключевые вещи: 1. Мгновенно показывает кэш 2. Сразу ставит миниатюры 3. Обновляет данные в фонеПоэтому визуально кажется, что: фотографии и счётчик появляются моментально. Исходной код: Код
<link rel="stylesheet" href="https://bro.usite.pro/css/vk-poss2.css" media="all"> <!-- Подключаем шрифт Roboto для аккуратного вида --> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&display=swap" rel="stylesheet"> <div id="user-photos"> <span id="vkPhotosCount"></span> </div> <script> window.UCOZ_DATA = { userId: "$USER_ID$", profileUrl: "$PERSONAL_PAGE_LINK$" }; // Сразу показываем кэшированное число const cachedCount = parseInt(localStorage.getItem('userPhotosCount_$USER_ID$')); if(!isNaN(cachedCount) && cachedCount > 0){ document.getElementById('vkPhotosCount').textContent = cachedCount; } </script> <script src="https://bro.usite.pro/js/profile-photos2.js?v=1.0"></script>
Исключительно в ознакомительных целях. Код
(function(){ function waitJQ(cb){ if(typeof jQuery === "undefined"){ setTimeout(()=>waitJQ(cb),100); }else{ cb(jQuery); } } waitJQ(function($){ $(function(){ if(!window.UCOZ_DATA) return; const userId = window.UCOZ_DATA.userId || "0"; const profileUrl = window.UCOZ_DATA.profileUrl || ""; if(userId==="0" || !profileUrl) return; const maxPhotos = 4; const container = $("#user-photos"); const counter = $("#vkPhotosCount"); if(!container.length) return; const LS_PHOTOS = "userPhotos_"+userId; const LS_COUNT = "userPhotosCount_"+userId; const LS_ALBUM = "userPhotosAlbum_"+userId; /* ---------------- utils ---------------- */ function normalize(url){ try{ const u = new URL(url,location.origin); u.search=""; u.hash=""; return u.toString().replace(/\/$/,""); }catch(e){ return url; } } function makeCounterLink(album,count){ if(!counter.length) return; if(count>0 && album){ if(!counter.parent("a").length){ counter.wrap('<a href="'+album+'" style="text-decoration:none;color:inherit;"></a>'); counter.css("cursor","pointer"); } }else{ if(counter.parent("a").length){ counter.unwrap(); counter.css("cursor","default"); } } } /* ---------------- render только миниатюры ---------------- */ function render(items, count, album){ const grid = $('<div class="profile-photo-grid"></div>'); if(items && items.length){ items.slice(0, maxPhotos).forEach(p => { const card = $('<a class="photo-card"></a>') .attr("href", p.link) // при клике идём на полноразмер .css({ display: "block", overflow: "hidden", borderRadius: "3px" }); // здесь сразу ставим миниатюру, без Base64 const img = $('<img loading="lazy" alt="Фото пользователя">') .attr("src", p.src) .css({ opacity: 1, display: "block" }); card.append(img); grid.append(card); }); container.empty().append(grid); } else { // пустая галерея container.html(` <div style="padding:24px 16px;text-align:center;color:#666;font-size:15px; background:#fafafa;border-radius:10px;min-height:110px; display:flex;flex-direction:column;justify-content:center;gap:10px;"> <div style="font-size:1.1em;color:#555;font-weight:500;">Пока здесь нет фотографий</div> <div style="font-size:.93em;color:#777;line-height:1.45;"> Добавьте свои снимки в<br> <a href="/photo/0-0-0-1-2" style="color:#3b82f6;text-decoration:none;font-weight:500;"> Фотоальбом </a><br> и они красиво отобразятся в профиле </div> </div> `); } // счётчик if(counter.length){ const cachedCount = parseInt(localStorage.getItem(LS_COUNT)); counter.text(!isNaN(cachedCount) ? cachedCount : count); } makeCounterLink(album, count); } /* ---------------- showCache ---------------- */ function showCache(){ try{ const photos = JSON.parse(localStorage.getItem(LS_PHOTOS) || "[]"); const cachedCount = parseInt(localStorage.getItem(LS_COUNT)); const album = localStorage.getItem(LS_ALBUM) || null; if(counter.length && !isNaN(cachedCount) && cachedCount > 0){ counter.text(cachedCount); } if(photos.length){ render(photos, cachedCount, album); } else { render([], cachedCount || 0, null); } } catch(e){ render([], 0, null); } } /* ---------------- load profile ---------------- */ function load(){ $.get(profileUrl).done(function(html){ const $page=$(html); let count=0; let album=null; $page.find("a").each(function(){ const t=$(this).text().trim(); const m=t.match(/^Фото\s*\((\d+)\)$/i); if(m){ count=parseInt(m[1]); album=this.href; return false; } }); if(!album || count===0){ localStorage.setItem(LS_PHOTOS,"[]"); localStorage.setItem(LS_COUNT,"0"); localStorage.removeItem(LS_ALBUM); render([],0,null); return; } localStorage.setItem(LS_ALBUM,album); /* -------- album -------- */ $.get(album).done(function(albumHtml){ const $album=$(albumHtml); const items=[]; $album.find("a.photo-card-title").each(function(){ let link=$(this).attr("href"); if(!link) return; if(link.startsWith("/")) link=location.origin+link; link=normalize(link); const img=$(this).closest(".entry-card").find("img[src*='/_ph/']").attr("src"); if(!img) return; const src=normalize(img); items.push({src,link}); if(items.length>=maxPhotos) return false; }); // обновляем кэш и DOM localStorage.setItem(LS_PHOTOS,JSON.stringify(items)); localStorage.setItem(LS_COUNT,count); render(items,count,album); }); }); } /* ---------------- start ---------------- */ showCache(); // сначала показываем кэш load(); // подтягиваем свежие данные, но DOM уже не дергается }); }); })();
Это видео демонстрирует реальную работу нового скрипта в действии. Разница ощутимая: он почти полностью воспроизводит поведение сложных, отлаженных систем именно то, к чему я стремился с самого начала.
Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.