Most chat UIs for local LLMs are built using heavy stack combinations: Next.js backend servers, React frameworks, Tailwind modules, and hundreds of dependency libraries. By the time the user loads the page to chat with their local model, the browser tab is already consuming **150MB to 300MB of client-side RAM** before the model even generates a single word.
We can build a fast, light interface using native **Vanilla JS**. By utilizing native streaming APIs, standard DOM methods, and CSS flexboxes, you can build a fully featured chat interface that compiles to a single 8KB HTML file and runs on **less than 0.5MB of RAM**.
Why Vanilla JS is Perfect for AI Streaming
Modern browser engines provide native helper tools for handling network streams. We don't need third-party WebSocket libraries or complex event state handlers to display real-time text streaming. We can use the browser's standard fetch() API coupled with a ReadableStream reader.
Step 1: The HTML Chat Structure
The interface needs a list of messages, a text input box, and a submit button. Below is the simple, semantic HTML snippet:
<div class="chat-container">
<div id="chat-messages" class="messages-list"></div>
<form id="chat-form" class="input-area">
<input type="text" id="chat-input" placeholder="Type a message..." required>
<button type="submit">Send</button>
</form>
</div>
Step 2: Parsing Response Streams in Real Time
When query data is sent to Ollama, it sends back a stream of JSON chunks containing individual words. We can capture this stream chunk-by-chunk using a loop that reads bytes, decodes them to text, and updates the chat card directly:
const messagesContainer = document.getElementById('chat-messages');
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
let messageHistory = [];
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const query = chatInput.value.trim();
if (!query) return;
// Append user message to panel
appendMessage('user', query);
chatInput.value = '';
// Create message placeholder for assistant stream
const assistantBubble = appendMessage('assistant', 'Thinking...');
messageHistory.push({ role: 'user', content: query });
try {
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'llama3.2:3b',
messages: messageHistory,
stream: true
})
});
if (!response.body) throw new Error('No readable stream.');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let accumulatedText = '';
assistantBubble.textContent = ''; // clear thinking state
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunkText = decoder.decode(value, { stream: true });
// Split by newline because Ollama outputs separate JSON objects per line
const lines = chunkText.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
const parsed = JSON.parse(line);
if (parsed.message && parsed.message.content) {
accumulatedText += parsed.message.content;
assistantBubble.textContent = accumulatedText;
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
}
messageHistory.push({ role: 'assistant', content: accumulatedText });
} catch (err) {
assistantBubble.textContent = `Error connecting to Ollama: ${err.message}`;
}
});
function appendMessage(sender, text) {
const bubble = document.createElement('div');
bubble.className = `message-bubble bubble-${sender}`;
bubble.textContent = text;
messagesContainer.appendChild(bubble);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return bubble;
}
Configuring Ollama CORS Settings
Because you are loading the static file from a local filesystem URL (file:///...) or a custom static host, your browser's security system will block requests to Ollama's local port (11434) unless CORS header settings are enabled.
To fix this, expose Ollama to cross-origin static files by setting the environment variable in your terminal before launching the Ollama application:
# Linux / macOS
export OLLAMA_ORIGINS="*"
ollama serve
# Windows (Command Prompt)
set OLLAMA_ORIGINS=*
ollama serve
Conclusion
By bypassing heavy Javascript rendering pipelines, the client page loads in less than 20 milliseconds, responds instantly to user interaction, and keeps memory overhead below 0.5MB. It proves that vanilla web components are more than enough to handle complex state pipelines like AI streaming.