A minimalist social app with a more ambitious frontend

Moments is built as a lightweight, minimalist social feed for sharing short updates, but its scope has grown well beyond a simple web timeline. Since v0.2.1, the backend has been rewritten in Golang, which reduces package size while expanding what the service can do. The app now offers a cleaner and more capable web-based sharing experience, with support for multiple content types and a structure that stays simple without feeling limited.

At its core, Moments works like a compact publishing space for everyday posts. It supports tag management for better organization, image uploads to either local storage or S3, and automatic thumbnail generation for locally uploaded images. Posts can be written with Markdown, and interaction features such as likes and comments are built in. Content can also be enriched with embedded NetEase Cloud Music, Bilibili videos, external links, and quoted information from Douban books and films.

The surrounding experience is just as practical. The interface adapts well to mobile screens, includes dark mode, offers a back-to-top button for long pages, and uses SQLite for easier backup and data portability. Users can also customize visual elements such as the header image, avatar, and site title.

Cleaning up the page and reshaping the presentation

The visual work starts with restraint. Some links and decorative elements were intentionally hidden with display: none so the page feels less cluttered and distractions are reduced. That cleanup creates space for a more focused presentation, especially once the player-related UI enters the scene.

The more distinctive change is the addition of a Dynamic Island-style lyric display. A dedicated .lyric-container is absolutely positioned to cover its parent area, then centered with flex layout so lyrics stay aligned both vertically and horizontally. overflow: hidden keeps any excess text out of view, and pointer-events: none prevents the lyric layer from interfering with other interactions.

The lyric text itself is designed to appear gently rather than abruptly. In its initial state, .lyric-text sits off to the right with zero opacity. When the visible class is applied, transitions move it into the center while fading it in. The scrolling animation was also adjusted to feel less rushed: animation: scrollLyric 25s linear infinite extends the lyric cycle to 25 seconds, and the @keyframes scrollLyric timing is redistributed so the entrance, pause, and exit phases feel more balanced.

Another visual enhancement comes from the Dynamic Island glow effect. A ::before pseudo-element adds a radial-gradient background inside .dynamic-island, initially hidden with zero opacity. Once the element enters the playing state, that glow fades in and starts the glowRotate animation, rotating on a 6-second loop to give the island a more vivid, animated presence.

Simulating an iPhone 16 Pro inside the page

One of the more playful but technically demanding ideas was to present the page as if it were running inside an iPhone 16 Pro. The approach is straightforward in concept: create a phone-shaped container, place the original page content inside it, and add a Dynamic Island layer on top to mimic familiar device behavior. In practice, that required careful handling of dimensions, layout, animation, and interaction.

Path checks before initialization

The simulation logic begins inside initiPhoneSimulator, where the first step is route validation. The effect only runs on the root path, preventing the simulated device frame from appearing where it is not needed and keeping the behavior targeted.

Size calculation and responsive fitting

The implementation defines baseWidth and baseHeight as the reference dimensions for the simulated phone, then derives a ratio from them. A minimum width of 395px is enforced with minWidth so the mock device never shrinks into something visually implausible.

Available space is then calculated from the current viewport: the maximum width is limited to 85% of screen width, and the maximum height to 95% of screen height. From there, the code compares the height implied by the base width against the available height. If the calculated height would exceed the limit, the height is capped first and the width is recalculated from the aspect ratio. A second pass ensures the width still respects the minimum value.

That sequence makes the simulation feel intentional rather than approximate. It preserves the phone’s proportions while still fitting naturally into different screens.

Building the simulated device

The phone shell itself is created with document.createElement and styled as a standalone div with explicit width, height, borders, rounded corners, background styling, and hover effects. Inside that shell sits a dedicated screen container, also given its own dimensions, background, and corner radius so the content area feels like a real display rather than a simple nested box.

The Dynamic Island is then added inside the screen container. Its position, size, background, and border radius are all defined manually, and it includes the lyric display and related interaction logic. Clicking the island changes its width and height so it can expand and collapse, making it feel less like a decorative layer and more like an active interface component.

A separate content container is used to host the original page. The existing __nuxt container is moved into this area, where width, height, and scrolling are controlled to match the simulated screen. Any fixed-width classes from the original page are removed so the content can adapt to the new device-like frame.

Listening to music players and updating lyrics

The bridge between the page content and the Dynamic Island is handled by setupAdvancedPlayerListener, which coordinates player state monitoring and lyric updates.

MutationObserver is used to watch both music players and play buttons for state changes. When playback starts, the Dynamic Island expands and begins reflecting the current lyric state. When playback pauses, the island resets and lyric updates stop.

To make this work across longer pages and dynamic loading, Intersection Observer watches for player elements entering the viewport. Once a player becomes visible, listener initialization begins. If a player later scrolls out of view, the listener is retained so background playback still behaves correctly.

There is also another MutationObserver watching for DOM changes. When new player elements are inserted into the page, they are automatically registered without manual intervention.

Cleanup on exit

All observers and timers are cleaned up through window.addEventListener('beforeunload') when the page unloads. That reduces the risk of memory leaks and keeps the feature from leaving unnecessary listeners behind.

iPhone-style Moments interface

The multi-player problem on dynamic pages

Once multiple music players are allowed to exist on the same page, especially in an infinite-scroll environment, several coordination issues appear at once.

The first is player state synchronization. If more than one player is present, the Dynamic Island has to identify which one is actively playing and switch its displayed information at the right moment.

The second is lyric timing and scrolling. Lyrics need to update in real time, and the island must scroll them smoothly without lagging or repeatedly re-rendering the same line.

The third is performance. If every player is observed too aggressively, or if lyric checks rely on excessive DOM polling, the page can become noticeably heavier.

The fourth is compatibility with infinite scrolling. Newly added players need to be detected automatically, and players that leave the viewport should not break playback behavior.

How those issues were handled

For playback synchronization, MutationObserver watches class changes on both the player and the play button. When a player enters a playing state, it is marked as the active one and the Dynamic Island expands accordingly. When it pauses, the island returns to its resting state.

For lyric updates, an earlier approach used setInterval to check the active player’s lyric state every 1.5 seconds. When a change was detected, the lyric content inside the Dynamic Island was refreshed and the scrolling animation restarted.

To keep that manageable across multiple players, Map is used to store each player’s MutationObserver reference. This avoids duplicate initialization and keeps listener management more organized.

Intersection Observer is further tuned with rootMargin and threshold so observation can begin slightly before a player fully enters the viewport. That helps reduce unnecessary DOM work while still keeping playback handling responsive.

And for infinite scrolling, a page-level MutationObserver watches for newly inserted DOM nodes. Any new player discovered in the document is automatically initialized.

Dynamic Island upgrades: better visuals, less wasted work

After the basic implementation was working, the Dynamic Island was improved in two directions at once: richer visual presentation and more efficient execution.

Richer styling: album art and clearer lyrics

A new album-cover container was added to the island, giving it more depth when music is playing. Through the .dynamic-island.playing state, the cover area expands automatically and displays the current artwork, making the link between the player and the island easier to read at a glance.

Lyric styling was also refined. Larger font sizing and tighter margin control improve readability, and a unified cubic-bezier(0.4, 0, 0.2, 1) transition curve makes the island expansion, cover reveal, and related animations feel more consistent.

Reworking the logic: from active polling to passive listening

The more important shift happened in JavaScript. Instead of relying mainly on timer-based polling, the lyric system was restructured around MutationObserver so updates happen when the lyrics actually change.

A lyric scrolling controller was introduced on the JavaScript side, and the lyric change detection became the central optimization.

Rather than querying the DOM at fixed intervals, MutationObserver now watches the lyric element—specifically the p tag—for class changes. A lyric update is triggered only when the current line actually switches. That means the system does not spend time repeatedly checking and reprocessing unchanged content.

Lyric text is also cleaned with regular expressions to remove translated portions when necessary, so the display stays focused on the primary line. Scrolling calculations only run when the lyric itself changes.

To improve motion quality, lyric scrolling uses requestAnimationFrame together with easing logic, producing smoother movement and less visual stutter than a simple timer-based update cycle.

Performance changes in practical terms

The difference between the old and new strategies is significant:

<table> <thead> <tr> <th>Dimension</th> <th>Original approach (timed polling)</th> <th>Optimized approach (MutationObserver)</th> </tr> </thead> <tbody> <tr> <td>Trigger timing</td> <td>Fixed intervals, such as once every 500ms whether lyrics changed or not</td> <td>Only when the lyric actually changes (p element class updates)</td> </tr> <tr> <td>DOM access frequency</td> <td>Frequent repeated queries</td> <td>A single query when the lyric changes</td> </tr> <tr> <td>Main thread usage</td> <td>Constant background cost even when lyrics remain the same</td> <td>Brief work only when a lyric switch occurs</td> </tr> <tr> <td>Wasteful computation</td> <td>High, because identical lyrics may be processed repeatedly</td> <td>Eliminated, since each line is handled once</td> </tr> <tr> <td>Resource cleanup</td> <td>Relies on timer cleanup and is easier to miss</td> <td>Uses observer.disconnect() for precise release</td> </tr> </tbody> </table>

The core improvement is that lyric-related DOM work is now tied directly to lyric changes rather than to an arbitrary time interval. Since lyric changes usually happen far less often than every 500ms, main-thread pressure drops sharply. The benefit becomes even more obvious when multiple players exist on the page or playback runs for long periods.

Synchronizing album art

Album art was also integrated into the Dynamic Island. By reading the cover element from the player—aplayer-pic—the system can extract the current image and mirror it into the island’s album-art container. This adds another layer of visual continuity between the embedded player and the simulated device UI.

Enhanced Dynamic Island with lyrics and album art

What this work changed

The updates to Moments do more than decorate the page. They reshape how the app is experienced.

On the surface, the interface becomes cleaner through selective element removal, more polished lyric presentation, and a Dynamic Island with stronger visual identity. Underneath that, the iPhone 16 Pro simulation gives the page a coherent device-like frame, making the interaction model feel more intentional.

Just as important, the logic behind the feature set has become more disciplined. Multi-player coordination, real-time lyric handling, infinite-scroll support, and album-art syncing are all managed in a way that reduces wasted work instead of piling on more timers and DOM queries.

There is still room to improve. Further performance tuning remains worthwhile, especially around reducing unnecessary DOM operations and keeping long-session playback as light as possible. Browser and device compatibility also deserve continued attention so the simulation and interaction model remain stable across more environments.