use page-map on emscripten to reduce the alignment requirements (leading to less memory usage); allow environment variables from emscripten, see also issue #1455
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0045689..fe63e72 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -464,7 +464,7 @@
 endif()
 
 if(MI_GUARDED STREQUAL "DEFAULT")
-  if(MI_DEBUG)
+  if(MI_DEBUG AND NOT (EMSCRIPTEN OR CMAKE_SYSTEM_NAME STREQUAL "WASI"))   # no memory protection on wasm
     set(MI_GUARDED "FULL")
   else()
     set(MI_GUARDED "OFF")
@@ -552,8 +552,8 @@
   list(APPEND mi_defines MI_PROFILE=0)
 endif()
 
-if(CMAKE_SYSTEM_NAME STREQUAL "WASI")
-  message(STATUS "Enabling MI_FREE_USE_PAGEMAP since the target (WASI) does not support large OS aligned allocations well")
+if(CMAKE_SYSTEM_NAME STREQUAL "WASI" OR EMSCRIPTEN)
+  message(STATUS "Enabling MI_FREE_USE_PAGEMAP since the target (${CMAKE_SYSTEM_NAME}) does not support large OS aligned allocations well")
   set(MI_FREE_USE_PAGEMAP ON)
 endif()
 if(MI_FREE_USE_PAGEMAP)
@@ -898,6 +898,9 @@
     list(APPEND mi_libraries "${MI_LIB_ATOMIC}")
   endif()
 endif()
+if(EMSCRIPTEN)
+  list(APPEND mi_libraries -sALLOW_MEMORY_GROWTH=1)  # mimalloc reserves memory in large chunks
+endif()
 
 # set language for source files now
 if(MI_USE_CXX)
@@ -1064,6 +1067,9 @@
   target_compile_definitions(mimalloc PRIVATE ${mi_defines} MI_SHARED_LIB MI_SHARED_LIB_EXPORT)
   target_compile_options(mimalloc PRIVATE ${mi_cflags} ${mi_cflags_dynamic})
   target_link_libraries(mimalloc PRIVATE ${mi_libraries})
+  if(EMSCRIPTEN)
+    target_link_options(mimalloc PRIVATE -Wno-pthreads-mem-growth)
+  endif()
   target_include_directories(mimalloc PUBLIC
       $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
       $<INSTALL_INTERFACE:${mi_install_incdir}>
@@ -1213,7 +1219,7 @@
   # run tests through the emulator when cross compiling (e.g. `node` for emscripten)
   set(mi_test_runner ${CMAKE_CROSSCOMPILING_EMULATOR})
   if(EMSCRIPTEN)
-    add_link_options(-sALLOW_MEMORY_GROWTH=1)
+    add_link_options(-sNODE_HOST_ENV=1)  # pass environment variables
     if(CMAKE_C_FLAGS MATCHES "-pthread")
       add_link_options(-pthread -sPTHREAD_POOL_SIZE=40 -Wno-pthreads-mem-growth)  # pre-spawn workers so threads can start while main blocks
     endif()
diff --git a/readme.md b/readme.md
index 0c423bb..74a0c9c 100644
--- a/readme.md
+++ b/readme.md
@@ -247,6 +247,22 @@
 > cmake ../.. -G "Visual Studio 17 2022" -T ClangCl
 ```
 
+## Emscripten
+
+You can build for WebAssembly with [Emscripten](https://emscripten.org) using `emcmake`.
+Add `-pthread` to the compiler flags for a multi-threaded build. The tests run under `node`:
+
+```
+> emcmake cmake ../.. -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-pthread
+> cmake --build .
+> ctest
+```
+
+When linking with mimalloc, use `-sALLOW_MEMORY_GROWTH` (as mimalloc reserves its arena memory up front; this is
+added automatically when linking with the CMake target or `pkg-config`), and `-pthread` if mimalloc was built with it.
+Mimalloc reads `MIMALLOC_` options from the program environment;
+under `node` you can pass the host environment with `-sNODE_HOST_ENV`, or otherwise set `ENV` in the JavaScript module.
+
 
 ## Single Source
 
diff --git a/src/prim/emscripten/prim.c b/src/prim/emscripten/prim.c
index 5dd6e5c..eccb662 100644
--- a/src/prim/emscripten/prim.c
+++ b/src/prim/emscripten/prim.c
@@ -9,6 +9,7 @@
 
 #include "mimalloc.h"
 #include "mimalloc/prim.h"
+#include "mimalloc/util/libc.h"
 #include "mimalloc/heap/init.h"
 
 #include <sched.h>   // sched_yield
@@ -72,6 +73,16 @@
 // Allocation
 //---------------------------------------------
 
+// The builtin allocator aborts on OOM by default (`-sABORTING_MALLOC`), so we fail early
+// if the heap cannot grow enough (e.g. without `-sALLOW_MEMORY_GROWTH`) and let mimalloc fall back.
+// (This is conservative as the builtin allocator may still have enough free space internally.)
+static bool mi_prim_heap_has_room(size_t size, size_t alignment) {
+  const uintptr_t top = *emscripten_get_sbrk_ptr();
+  const size_t max = emscripten_get_heap_max();
+  const size_t needed = size + alignment + 64*MI_KiB;  // allow for alignment, overhead, and sbrk granularity
+  return (needed > size && top < max && needed <= max - top);
+}
+
 // Note: the `try_alignment` is just a hint and the returned pointer is not guaranteed to be aligned.
 int _mi_prim_alloc(void* hint_addr, size_t size, size_t try_alignment, bool commit, bool allow_large, bool* is_large, bool* is_zero, void** addr) {
   MI_UNUSED(try_alignment); MI_UNUSED(allow_large); MI_UNUSED(commit); MI_UNUSED(hint_addr);
@@ -85,6 +96,10 @@
   if (try_alignment < MIN_EMMALLOC_ALIGN) {
     try_alignment = MIN_EMMALLOC_ALIGN;
   }
+  if (!mi_prim_heap_has_room(size, try_alignment)) {
+    *addr = NULL;
+    return ENOMEM;
+  }
   void* p = emscripten_builtin_memalign(try_alignment, size);
   *addr = p;
   if (p == 0) {
@@ -185,11 +200,23 @@
 // Environment
 //----------------------------------------------------------------
 
+// Under node, host environment variables are only visible when linking with `-sNODE_HOST_ENV=1`.
+// (Define `MI_NO_GETENV` to exclude this code)
+extern char** environ;
+
 int _mi_prim_getenv(const char* name, char* result, size_t result_size) {
-  // For code size reasons, do not support environ customization for now.
-  MI_UNUSED(name);
-  MI_UNUSED(result);
-  MI_UNUSED(result_size);
+  if (name==NULL) return -1;
+  const size_t len = _mi_strlen(name);
+  if (len == 0) return -1;
+  char** env = environ;
+  if (env == NULL) return -1;  // not yet initialized; try again later
+  for (int i = 0; i < 10000 && env[i] != NULL; i++) {
+    const char* s = env[i];
+    if (_mi_strnicmp(name, s, len) == 0 && s[len] == '=') { // case insensitive
+      if (!_mi_strlcpy(result, s + len + 1, result_size)) return -1;
+      return 1;
+    }
+  }
   return 0; // not found
 }
 
diff --git a/src/util/options.c b/src/util/options.c
index 5940532..3ce1316 100644
--- a/src/util/options.c
+++ b/src/util/options.c
@@ -603,6 +603,8 @@
 #if MI_DEBUG
 mi_decl_noreturn mi_decl_cold void _mi_assert_fail(const char* assertion, const char* fname, unsigned line, const char* func ) mi_attr_noexcept {
   _mi_fprintf(NULL, NULL, "mimalloc: assertion failed: at \"%s\":%u, %s\n  assertion: \"%s\"\n", fname, line, (func==NULL?"":func), assertion);
+  // flush delayed output (if not yet initialized) so the message is not lost
+  if (mi_atomic_load_ptr_acquire(void,&mi_out_default) == NULL) { mi_out_buf_flush(&mi_out_stderr, true, NULL); }
   abort();
 }
 #endif